+ {post.frontmatter.title} +
+ ++ {post.frontmatter.description} +
+From 3d6d4ed251688764a422f505de8a7707c08e6bf3 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:27:29 -0600 Subject: [PATCH 01/48] Infrastructure setup --- .runner.pid | 1 - content/blog/resolving-active-conflicts.md | 110 ++ lib/content.js | 155 ++ lib/validation.js | 154 ++ next.config.mjs | 11 +- package-lock.json | 1809 +++++++++++++++++++- package.json | 5 + tests/unit/content.test.js | 308 ++++ tests/unit/validation.test.js | 168 ++ 9 files changed, 2709 insertions(+), 12 deletions(-) delete mode 100644 .runner.pid create mode 100644 content/blog/resolving-active-conflicts.md create mode 100644 lib/content.js create mode 100644 lib/validation.js create mode 100644 tests/unit/content.test.js create mode 100644 tests/unit/validation.test.js diff --git a/.runner.pid b/.runner.pid deleted file mode 100644 index 55afcf7..0000000 --- a/.runner.pid +++ /dev/null @@ -1 +0,0 @@ -10574 diff --git a/content/blog/resolving-active-conflicts.md b/content/blog/resolving-active-conflicts.md new file mode 100644 index 0000000..bac9ee9 --- /dev/null +++ b/content/blog/resolving-active-conflicts.md @@ -0,0 +1,110 @@ +--- +title: "Resolving Active Conflicts" +description: "Practical steps for resolving conflicts while maintaining trust, cooperation, and shared goals" +author: "Community Organizer" +date: "2025-04-15" +tags: ["conflict-resolution", "governance", "community"] +related: ["operational-security", "making-decisions-without-hierarchy"] +--- + +# Resolving Active Conflicts + +Practical steps for resolving conflicts while maintaining trust, cooperation, and shared goals. + +## Understanding Conflict in Communities + +Conflict is a natural part of any community's growth and evolution. When people come together with different perspectives, experiences, and goals, disagreements are inevitable. The key is not to avoid conflict, but to handle it constructively. + +## The Foundation: Trust and Communication + +Before any conflict resolution process can be effective, there must be a foundation of trust and open communication. Community members need to feel safe expressing their concerns and confident that their voices will be heard. + +### Building Trust + +- Regular check-ins and community meetings +- Transparent decision-making processes +- Clear communication channels +- Consistent follow-through on commitments + +## A Structured Approach to Resolution + +### 1. Acknowledge the Conflict + +The first step is recognizing that a conflict exists and needs attention. Ignoring conflicts often makes them worse. + +### 2. Create Safe Space for Discussion + +- Choose a neutral location +- Set ground rules for respectful communication +- Ensure all parties have equal time to speak +- Use a facilitator if needed + +### 3. Identify Root Causes + +Look beyond surface-level disagreements to understand the underlying issues: + +- Unmet needs or expectations +- Misunderstandings or miscommunications +- Competing priorities or values +- Resource constraints + +### 4. Generate Solutions Together + +- Brainstorm multiple options +- Focus on interests, not positions +- Look for win-win solutions +- Consider creative alternatives + +### 5. Agree on Next Steps + +- Document the agreed-upon solution +- Assign responsibilities and timelines +- Set up follow-up meetings +- Establish how to handle future conflicts + +## Tools and Techniques + +### Active Listening + +- Give full attention to the speaker +- Reflect back what you heard +- Ask clarifying questions +- Avoid interrupting or planning your response + +### Nonviolent Communication + +- Observe without judgment +- Express feelings and needs clearly +- Make specific, actionable requests +- Show empathy for others' perspectives + +### Consensus Building + +- Seek solutions that work for everyone +- Build on areas of agreement +- Address concerns and objections +- Work toward genuine consensus, not just majority rule + +## When to Seek External Help + +Some conflicts may require outside assistance: + +- When emotions are running very high +- When there's a power imbalance +- When the conflict involves legal issues +- When previous attempts at resolution have failed + +## Prevention and Maintenance + +The best conflict resolution is prevention: + +- Regular community health checks +- Clear policies and procedures +- Ongoing relationship building +- Early intervention when tensions arise + +## Conclusion + +Conflict resolution is not about eliminating disagreements, but about creating a community where conflicts can be addressed constructively and lead to growth and understanding. With the right tools, processes, and commitment, conflicts can become opportunities for strengthening relationships and improving community governance. + +Remember: the goal is not to win arguments, but to build stronger, more resilient communities where everyone can thrive. diff --git a/lib/content.js b/lib/content.js new file mode 100644 index 0000000..2c284c5 --- /dev/null +++ b/lib/content.js @@ -0,0 +1,155 @@ +import fs from "fs"; +import path from "path"; +import matter from "gray-matter"; +import { validateBlogPost, sanitizeBlogPost } from "./validation.js"; + +/** + * Content processing utilities for blog posts + */ + +/** + * Get all blog post files from the content directory + * @returns {Array} Array of file paths + */ +export function getBlogPostFiles() { + const contentDirectory = path.join(process.cwd(), "content/blog"); + + try { + const files = fs.readdirSync(contentDirectory); + return files.filter( + (file) => file.endsWith(".md") || file.endsWith(".mdx") + ); + } catch (error) { + console.error("Error reading blog content directory:", error); + return []; + } +} + +/** + * Parse a single blog post file + * @param {string} filePath - Path to the markdown file + * @returns {Object|null} Parsed blog post data or null if invalid + */ +export function parseBlogPost(filePath) { + try { + const fullPath = path.join(process.cwd(), "content/blog", filePath); + const fileContents = fs.readFileSync(fullPath, "utf8"); + const { data: frontmatter, content } = matter(fileContents); + + // Validate frontmatter + const validation = validateBlogPost(frontmatter); + if (!validation.isValid) { + console.error(`Validation failed for ${filePath}:`, validation.errors); + return null; + } + + // Sanitize frontmatter + const sanitized = sanitizeBlogPost(frontmatter); + + // Generate slug from filename + const slug = filePath.replace(/\.(md|mdx)$/, ""); + + return { + slug, + frontmatter: sanitized, + content, + filePath, + }; + } catch (error) { + console.error(`Error parsing blog post ${filePath}:`, error); + return null; + } +} + +/** + * Get all blog posts with parsed data + * @returns {Array} Array of parsed blog post objects + */ +export function getAllBlogPosts() { + const files = getBlogPostFiles(); + const posts = files + .map((file) => parseBlogPost(file)) + .filter((post) => post !== null) + .sort( + (a, b) => new Date(b.frontmatter.date) - new Date(a.frontmatter.date) + ); + + return posts; +} + +/** + * Get a single blog post by slug + * @param {string} slug - The post slug + * @returns {Object|null} Parsed blog post or null if not found + */ +export function getBlogPostBySlug(slug) { + const files = getBlogPostFiles(); + const file = files.find((f) => f.replace(/\.(md|mdx)$/, "") === slug); + + if (!file) { + return null; + } + + return parseBlogPost(file); +} + +/** + * Get related blog posts + * @param {string} currentSlug - Current post slug + * @param {Array} relatedSlugs - Array of related post slugs + * @param {number} limit - Maximum number of related posts to return + * @returns {Array} Array of related blog posts + */ +export function getRelatedBlogPosts(currentSlug, relatedSlugs = [], limit = 3) { + if (!relatedSlugs || relatedSlugs.length === 0) { + // Fallback: get posts with similar tags or recent posts + const allPosts = getAllBlogPosts(); + return allPosts.filter((post) => post.slug !== currentSlug).slice(0, limit); + } + + const allPosts = getAllBlogPosts(); + const related = allPosts + .filter((post) => relatedSlugs.includes(post.slug)) + .slice(0, limit); + + // If we don't have enough related posts, fill with recent ones + if (related.length < limit) { + const recent = allPosts + .filter( + (post) => post.slug !== currentSlug && !relatedSlugs.includes(post.slug) + ) + .slice(0, limit - related.length); + return [...related, ...recent]; + } + + return related; +} + +/** + * Get all unique tags from blog posts + * @returns {Array} Array of unique tags + */ +export function getAllTags() { + const posts = getAllBlogPosts(); + const tags = new Set(); + + posts.forEach((post) => { + if (post.frontmatter.tags) { + post.frontmatter.tags.forEach((tag) => tags.add(tag)); + } + }); + + return Array.from(tags).sort(); +} + +/** + * Get blog posts by tag + * @param {string} tag - Tag to filter by + * @returns {Array} Array of blog posts with the specified tag + */ +export function getBlogPostsByTag(tag) { + const posts = getAllBlogPosts(); + return posts.filter( + (post) => post.frontmatter.tags && post.frontmatter.tags.includes(tag) + ); +} diff --git a/lib/validation.js b/lib/validation.js new file mode 100644 index 0000000..379a3d8 --- /dev/null +++ b/lib/validation.js @@ -0,0 +1,154 @@ +/** + * Content validation utilities for blog posts + */ + +/** + * Blog post frontmatter schema + */ +export const BLOG_POST_SCHEMA = { + title: { + type: "string", + required: true, + minLength: 1, + maxLength: 100, + }, + description: { + type: "string", + required: true, + minLength: 10, + maxLength: 200, + }, + author: { + type: "string", + required: true, + minLength: 1, + maxLength: 50, + }, + date: { + type: "string", + required: true, + pattern: /^\d{4}-\d{2}-\d{2}$/, // YYYY-MM-DD format + }, + tags: { + type: "array", + required: false, + default: [], + items: { + type: "string", + minLength: 1, + maxLength: 20, + }, + }, + related: { + type: "array", + required: false, + default: [], + items: { + type: "string", + minLength: 1, + maxLength: 50, + }, + }, +}; + +/** + * Validate a blog post's frontmatter + * @param {Object} frontmatter - The frontmatter object to validate + * @returns {Object} Validation result with isValid boolean and errors array + */ +export function validateBlogPost(frontmatter) { + const errors = []; + + // Check required fields first + for (const [field, config] of Object.entries(BLOG_POST_SCHEMA)) { + if (config.required && !frontmatter[field]) { + errors.push(`Missing required field: ${field}`); + } + } + + // If we have missing required fields, don't continue with other validations + if (errors.length > 0) { + return { + isValid: false, + errors, + }; + } + + // Now validate field types and constraints + for (const [field, config] of Object.entries(BLOG_POST_SCHEMA)) { + if (frontmatter[field] !== undefined) { + // Type validation + if (config.type === "string" && typeof frontmatter[field] !== "string") { + errors.push(`Field ${field} must be a string`); + } else if ( + config.type === "array" && + !Array.isArray(frontmatter[field]) + ) { + errors.push(`Field ${field} must be an array`); + } + + // Length validation for strings + if (config.type === "string" && typeof frontmatter[field] === "string") { + if (config.minLength && frontmatter[field].length < config.minLength) { + errors.push( + `Field ${field} must be at least ${config.minLength} characters` + ); + } + if (config.maxLength && frontmatter[field].length > config.maxLength) { + errors.push( + `Field ${field} must be no more than ${config.maxLength} characters` + ); + } + } + + // Pattern validation + if (config.pattern && !config.pattern.test(frontmatter[field])) { + errors.push(`Field ${field} format is invalid`); + } + + // Array item validation + if (config.type === "array" && Array.isArray(frontmatter[field])) { + for (let i = 0; i < frontmatter[field].length; i++) { + const item = frontmatter[field][i]; + if (config.items.type === "string" && typeof item !== "string") { + errors.push(`Item ${i} in ${field} must be a string`); + } + if (config.items.minLength && item.length < config.items.minLength) { + errors.push( + `Item ${i} in ${field} must be at least ${config.items.minLength} characters` + ); + } + if (config.items.maxLength && item.length > config.items.maxLength) { + errors.push( + `Item ${i} in ${field} must be no more than ${config.items.maxLength} characters` + ); + } + } + } + } + } + + return { + isValid: errors.length === 0, + errors, + }; +} + +/** + * Sanitize and normalize frontmatter data + * @param {Object} frontmatter - Raw frontmatter data + * @returns {Object} Sanitized frontmatter + */ +export function sanitizeBlogPost(frontmatter) { + const sanitized = {}; + + for (const [field, config] of Object.entries(BLOG_POST_SCHEMA)) { + if (frontmatter[field] !== undefined) { + sanitized[field] = frontmatter[field]; + } else if (config.default !== undefined) { + sanitized[field] = config.default; + } + } + + return sanitized; +} diff --git a/next.config.mjs b/next.config.mjs index ca50116..9f27b3d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,3 +1,5 @@ +import createMDX from "@next/mdx"; + /** @type {import('next').NextConfig} */ const nextConfig = { eslint: { @@ -13,4 +15,11 @@ const nextConfig = { }, }; -export default nextConfig; +const withMDX = createMDX({ + options: { + remarkPlugins: [], + rehypePlugins: [], + }, +}); + +export default withMDX(nextConfig); diff --git a/package-lock.json b/package-lock.json index 5accb96..4d3dd6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,10 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { + "@mdx-js/loader": "^3.1.1", + "@mdx-js/react": "^3.1.1", + "@next/mdx": "^15.5.2", + "gray-matter": "^4.0.3", "next": "15.2.4", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -35,6 +39,7 @@ "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/mdx": "^2.0.13", "@types/react": "19.1.12", "@typescript-eslint/eslint-plugin": "^8.41.0", "@typescript-eslint/parser": "^8.41.0", @@ -4319,11 +4324,96 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@mdx-js/loader": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/loader/-/loader-3.1.1.tgz", + "integrity": "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ==", + "license": "MIT", + "dependencies": { + "@mdx-js/mdx": "^3.0.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "webpack": ">=5" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/@mdx-js/loader/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/mdx/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@mdx-js/mdx/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/@mdx-js/react": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", - "dev": true, "license": "MIT", "dependencies": { "@types/mdx": "^2.0.0" @@ -4414,6 +4504,36 @@ "node": ">= 6" } }, + "node_modules/@next/mdx": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/mdx/-/mdx-15.5.2.tgz", + "integrity": "sha512-Lz9mdoKRfSNc7T1cSk3gzryhRcc7ErsiAWba1HBoInCX4ZpGUQXmiZLAAyrIgDl7oS/UHxsgKtk2qp/Df4gKBg==", + "license": "MIT", + "dependencies": { + "source-map": "^0.7.0" + }, + "peerDependencies": { + "@mdx-js/loader": ">=0.15.0", + "@mdx-js/react": ">=0.15.0" + }, + "peerDependenciesMeta": { + "@mdx-js/loader": { + "optional": true + }, + "@mdx-js/react": { + "optional": true + } + } + }, + "node_modules/@next/mdx/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/@next/swc-darwin-arm64": { "version": "15.2.4", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz", @@ -6818,6 +6938,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -6836,9 +6965,17 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -6849,6 +6986,15 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -6890,11 +7036,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/mdx": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/node": { @@ -6938,6 +7098,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/wait-on": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/@types/wait-on/-/wait-on-5.3.4.tgz", @@ -7260,6 +7426,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", @@ -7844,7 +8016,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -7857,7 +8028,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -8247,6 +8417,15 @@ "dev": true, "license": "MIT" }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -8514,6 +8693,16 @@ "@babel/core": "^7.0.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -8955,6 +9144,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -8999,6 +9198,46 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -9207,6 +9446,16 @@ "node": ">= 0.12.0" } }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -9272,6 +9521,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -9811,7 +10070,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -9842,6 +10100,19 @@ "dev": true, "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dedent": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", @@ -9985,7 +10256,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10022,6 +10292,19 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/devtools-protocol": { "version": "0.0.1467305", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1467305.tgz", @@ -10493,6 +10776,38 @@ "dev": true, "license": "MIT" }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", @@ -11122,7 +11437,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -11168,6 +11482,106 @@ "node": ">=4.0" } }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -11413,6 +11827,24 @@ "dev": true, "license": "MIT" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -12271,6 +12703,43 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -12392,6 +12861,74 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/headers-polyfill": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", @@ -12652,6 +13189,12 @@ "dev": true, "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, "node_modules/inquirer": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", @@ -12816,6 +13359,30 @@ "node": ">= 0.10" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -12981,6 +13548,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -12997,6 +13574,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -13075,6 +13661,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -13145,6 +13741,18 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -15078,6 +15686,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -15770,6 +16387,16 @@ "url": "https://tidelift.com/funding/github/npm/loglevel" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lookup-closest-locale": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", @@ -15901,6 +16528,18 @@ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", "dev": true }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/marky": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", @@ -15918,6 +16557,176 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -15989,6 +16798,602 @@ "node": ">= 0.6" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -16169,7 +17574,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/msw": { @@ -17293,6 +18697,31 @@ "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", "dev": true }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -17774,6 +19203,16 @@ "dev": true, "license": "MIT" }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -18095,6 +19534,73 @@ "node": ">=4" } }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -18237,6 +19743,21 @@ "node": ">=6" } }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -18250,6 +19771,53 @@ "node": ">=4" } }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -18663,6 +20231,19 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -19107,6 +20688,16 @@ "source-map": "^0.6.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -19224,7 +20815,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/stable-hash": { @@ -19681,6 +21271,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -19734,6 +21338,15 @@ "node": ">=8" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -19793,6 +21406,24 @@ "dev": true, "license": "MIT" }, + "node_modules/style-to-js": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", + "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.9" + } + }, + "node_modules/style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -20272,6 +21903,26 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -20589,6 +22240,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -20602,6 +22272,87 @@ "node": ">=8" } }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -20754,6 +22505,34 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz", @@ -21800,6 +23579,16 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index d7e5932..afe21ef 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,10 @@ "visual:ui": "npx playwright test tests/e2e/visual-regression.spec.ts --ui" }, "dependencies": { + "@mdx-js/loader": "^3.1.1", + "@mdx-js/react": "^3.1.1", + "@next/mdx": "^15.5.2", + "gray-matter": "^4.0.3", "next": "15.2.4", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -62,6 +66,7 @@ "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/mdx": "^2.0.13", "@types/react": "19.1.12", "@typescript-eslint/eslint-plugin": "^8.41.0", "@typescript-eslint/parser": "^8.41.0", diff --git a/tests/unit/content.test.js b/tests/unit/content.test.js new file mode 100644 index 0000000..2fff0d2 --- /dev/null +++ b/tests/unit/content.test.js @@ -0,0 +1,308 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { + getBlogPostFiles, + parseBlogPost, + getAllBlogPosts, + getBlogPostBySlug, + getRelatedBlogPosts, + getAllTags, + getBlogPostsByTag, +} from "../../lib/content.js"; + +// Mock fs and path modules +vi.mock("fs", () => ({ + readdirSync: vi.fn(), + readFileSync: vi.fn(), +})); + +vi.mock("path", () => ({ + join: vi.fn(), +})); + +describe("Content Processing", () => { + let mockReaddirSync, mockReadFileSync, mockPathJoin; + + beforeEach(() => { + vi.clearAllMocks(); + + // Get references to the mocked functions + const fs = require("fs"); + const path = require("path"); + mockReaddirSync = fs.readdirSync; + mockReadFileSync = fs.readFileSync; + mockPathJoin = path.join; + + // Mock process.cwd to return a predictable path + vi.spyOn(process, "cwd").mockReturnValue("/mock/project/root"); + + // Mock path.join to return predictable paths + mockPathJoin.mockImplementation((...args) => args.join("/")); + }); + + describe("getBlogPostFiles", () => { + it("should return markdown files from content directory", () => { + const mockFiles = ["post1.md", "post2.mdx", "image.png", "post3.md"]; + mockReaddirSync.mockReturnValue(mockFiles); + + const result = getBlogPostFiles(); + expect(result).toEqual(["post1.md", "post2.mdx", "post3.md"]); + expect(mockReaddirSync).toHaveBeenCalledWith( + "/mock/project/root/content/blog" + ); + }); + + it("should handle directory read errors gracefully", () => { + mockReaddirSync.mockImplementation(() => { + throw new Error("Directory not found"); + }); + + const result = getBlogPostFiles(); + expect(result).toEqual([]); + expect(mockReaddirSync).toHaveBeenCalledWith( + "/mock/project/root/content/blog" + ); + }); + }); + + describe("parseBlogPost", () => { + it("should parse a valid markdown file", () => { + const mockContent = `--- +title: "Test Post" +description: "A test description that meets the minimum length requirement" +author: "Test Author" +date: "2025-04-15" +tags: ["test"] +related: [] +--- +# Test Content +This is the content.`; + + mockReadFileSync.mockReturnValue(mockContent); + + const result = parseBlogPost("test-post.md"); + expect(result).toMatchObject({ + slug: "test-post", + frontmatter: { + title: "Test Post", + description: + "A test description that meets the minimum length requirement", + author: "Test Author", + date: "2025-04-15", + tags: ["test"], + related: [], + }, + content: "\n# Test Content\nThis is the content.", + filePath: "test-post.md", + }); + expect(mockReadFileSync).toHaveBeenCalledWith( + "/mock/project/root/content/blog/test-post.md", + "utf8" + ); + }); + + it("should return null for invalid frontmatter", () => { + const mockContent = `--- +title: "" # Invalid title +description: "A test description" +author: "Test Author" +date: "2025-04-15" +--- +# Test Content`; + + mockReadFileSync.mockReturnValue(mockContent); + + const result = parseBlogPost("invalid-post.md"); + expect(result).toBeNull(); + }); + + it("should handle file read errors gracefully", () => { + mockReadFileSync.mockImplementation(() => { + throw new Error("File not found"); + }); + + const result = parseBlogPost("non-existent-post.md"); + expect(result).toBeNull(); + }); + }); + + describe("getAllBlogPosts", () => { + it("should return all valid blog posts sorted by date", () => { + const mockFiles = ["post1.md", "post2.md", "post3.md"]; + mockReaddirSync.mockReturnValue(mockFiles); + + // Mock fs.readFileSync for each post + mockReadFileSync.mockReturnValueOnce(`--- +title: "Post 1" +description: "Desc 1" +author: "Author 1" +date: "2025-04-10" +--- +# Content 1`).mockReturnValueOnce(`--- +title: "Post 2" +description: "Desc 2" +author: "Author 2" +date: "2025-04-20" +--- +# Content 2`).mockReturnValueOnce(`--- +title: "Post 3" +description: "Desc 3" +author: "Author 3" +date: "2025-04-05" +--- +# Content 3`); + + const result = getAllBlogPosts(); + expect(result).toHaveLength(3); + expect(result[0].slug).toBe("post2"); // Latest date + expect(result[1].slug).toBe("post1"); + expect(result[2].slug).toBe("post3"); // Oldest date + }); + }); + + describe("getBlogPostBySlug", () => { + it("should return blog post for valid slug", () => { + const mockFiles = ["test-post.md"]; + mockReaddirSync.mockReturnValue(mockFiles); + + const mockContent = `--- +title: "Test Post" +description: "A test description that meets the minimum length requirement" +author: "Test Author" +date: "2025-04-15" +--- +# Test Content`; + + mockReadFileSync.mockReturnValue(mockContent); + + const result = getBlogPostBySlug("test-post"); + expect(result).not.toBeNull(); + expect(result.slug).toBe("test-post"); + }); + + it("should return null for invalid slug", () => { + const mockFiles = ["test-post.md"]; + mockReaddirSync.mockReturnValue(mockFiles); + + const result = getBlogPostBySlug("invalid-slug"); + expect(result).toBeNull(); + }); + }); + + describe("getRelatedBlogPosts", () => { + it("should return related posts when slugs are provided", () => { + const mockFiles = ["post1.md", "post2.md", "post3.md"]; + mockReaddirSync.mockReturnValue(mockFiles); + + // Mock content for all posts + mockReadFileSync.mockReturnValueOnce(`--- +title: "Post 1" +description: "Desc 1" +author: "Author 1" +date: "2025-04-10" +related: ["post2"] +--- +# Content 1`).mockReturnValueOnce(`--- +title: "Post 2" +description: "Desc 2" +author: "Author 2" +date: "2025-04-20" +--- +# Content 2`).mockReturnValueOnce(`--- +title: "Post 3" +description: "Desc 3" +author: "Author 3" +date: "2025-04-05" +--- +# Content 3`); + + const result = getRelatedBlogPosts("post1", ["post2", "post3"], 2); + expect(result).toHaveLength(2); + expect(result[0].slug).toBe("post2"); + expect(result[1].slug).toBe("post3"); + }); + + it("should fallback to recent posts when no related slugs provided", () => { + const mockFiles = ["post1.md", "post2.md", "post3.md"]; + mockReaddirSync.mockReturnValue(mockFiles); + + const mockContent = `--- +title: "Post 1" +description: "Desc 1" +author: "Author 1" +date: "2025-04-10" +--- +# Content 1`; + + mockReadFileSync.mockReturnValue(mockContent); + + const result = getRelatedBlogPosts("post1", [], 2); + expect(result).toHaveLength(2); + expect(result[0].slug).toBe("post2"); // Should be the most recent after excluding 'post1' + expect(result[1].slug).toBe("post3"); + }); + }); + + describe("getAllTags", () => { + it("should return unique tags from all posts", () => { + const mockFiles = ["post1.md", "post2.md"]; + mockReaddirSync.mockReturnValue(mockFiles); + + const mockContent1 = `--- +title: "Post 1" +description: "Desc 1" +author: "Author 1" +date: "2025-04-10" +tags: ["tagA", "tagB"] +--- +# Content 1`; + const mockContent2 = `--- +title: "Post 2" +description: "Desc 2" +author: "Author 2" +date: "2025-04-20" +tags: ["tagB", "tagC"] +--- +# Content 2`; + + mockReadFileSync + .mockReturnValueOnce(mockContent1) + .mockReturnValueOnce(mockContent2); + + const result = getAllTags(); + expect(result).toEqual(expect.arrayContaining(["tagA", "tagB", "tagC"])); + expect(result).toHaveLength(3); + }); + }); + + describe("getBlogPostsByTag", () => { + it("should return posts with matching tag", () => { + const mockFiles = ["post1.md", "post2.md"]; + mockReaddirSync.mockReturnValue(mockFiles); + + const mockContent1 = `--- +title: "Post 1" +description: "Desc 1" +author: "Author 1" +date: "2025-04-10" +tags: ["tagA", "tagB"] +--- +# Content 1`; + const mockContent2 = `--- +title: "Post 2" +description: "Desc 2" +author: "Author 2" +date: "2025-04-20" +tags: ["tagB", "tagC"] +--- +# Content 2`; + + mockReadFileSync + .mockReturnValueOnce(mockContent1) + .mockReturnValueOnce(mockContent2); + + const result = getBlogPostsByTag("tagA"); + expect(result).toHaveLength(1); + expect(result[0].slug).toBe("post1"); + }); + }); +}); diff --git a/tests/unit/validation.test.js b/tests/unit/validation.test.js new file mode 100644 index 0000000..a4a2527 --- /dev/null +++ b/tests/unit/validation.test.js @@ -0,0 +1,168 @@ +import { describe, it, expect } from "vitest"; +import { + validateBlogPost, + sanitizeBlogPost, + BLOG_POST_SCHEMA, +} from "../../lib/validation.js"; + +describe("Blog Post Validation", () => { + describe("validateBlogPost", () => { + it("should validate a correct blog post", () => { + const validPost = { + title: "Test Title", + description: + "This is a test description that meets the minimum length requirement", + author: "Test Author", + date: "2025-04-15", + tags: ["test", "blog"], + related: ["post-1", "post-2"], + }; + + const result = validateBlogPost(validPost); + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("should reject missing required fields", () => { + const invalidPost = { + title: "Test Title", + // Missing description, author, date + tags: ["test"], + }; + + const result = validateBlogPost(invalidPost); + expect(result.isValid).toBe(false); + expect(result.errors).toContain("Missing required field: description"); + expect(result.errors).toContain("Missing required field: author"); + expect(result.errors).toContain("Missing required field: date"); + }); + + it("should validate title length constraints", () => { + const shortTitle = { + title: "", // Empty string (less than 1 character minimum) + description: + "This is a test description that meets the minimum length requirement", + author: "Test Author", + date: "2025-04-15", + }; + + const result = validateBlogPost(shortTitle); + expect(result.isValid).toBe(false); + expect(result.errors).toContain("Missing required field: title"); + }); + + it("should validate date format", () => { + const invalidDate = { + title: "Test Title", + description: + "This is a test description that meets the minimum length requirement", + author: "Test Author", + date: "2025/04/15", // Wrong format + }; + + const result = validateBlogPost(invalidDate); + expect(result.isValid).toBe(false); + expect(result.errors).toContain("Field date format is invalid"); + }); + + it("should validate tags array", () => { + const invalidTags = { + title: "Test Title", + description: + "This is a test description that meets the minimum length requirement", + author: "Test Author", + date: "2025-04-15", + tags: "not-an-array", // Should be array + }; + + const result = validateBlogPost(invalidTags); + expect(result.isValid).toBe(false); + expect(result.errors).toContain("Field tags must be an array"); + }); + + it("should validate tag item lengths", () => { + const invalidTagItems = { + title: "Test Title", + description: + "This is a test description that meets the minimum length requirement", + author: "Test Author", + date: "2025-04-15", + tags: ["", "very-long-tag-name-that-exceeds-maximum-length"], + }; + + const result = validateBlogPost(invalidTagItems); + expect(result.isValid).toBe(false); + expect(result.errors).toContain( + "Item 0 in tags must be at least 1 characters" + ); + expect(result.errors).toContain( + "Item 1 in tags must be no more than 20 characters" + ); + }); + }); + + describe("sanitizeBlogPost", () => { + it("should return original data when all fields are present", () => { + const post = { + title: "Test Title", + description: "Test description", + author: "Test Author", + date: "2025-04-15", + tags: ["test"], + related: ["post-1"], + }; + + const sanitized = sanitizeBlogPost(post); + expect(sanitized).toEqual(post); + }); + + it("should add default values for missing optional fields", () => { + const post = { + title: "Test Title", + description: "Test description", + author: "Test Author", + date: "2025-04-15", + // Missing tags and related + }; + + const sanitized = sanitizeBlogPost(post); + expect(sanitized.tags).toEqual([]); + expect(sanitized.related).toEqual([]); + }); + + it("should preserve existing optional fields", () => { + const post = { + title: "Test Title", + description: "Test description", + author: "Test Author", + date: "2025-04-15", + tags: ["custom-tag"], + related: ["custom-post"], + }; + + const sanitized = sanitizeBlogPost(post); + expect(sanitized.tags).toEqual(["custom-tag"]); + expect(sanitized.related).toEqual(["custom-post"]); + }); + }); + + describe("BLOG_POST_SCHEMA", () => { + it("should have correct structure", () => { + expect(BLOG_POST_SCHEMA).toHaveProperty("title"); + expect(BLOG_POST_SCHEMA).toHaveProperty("description"); + expect(BLOG_POST_SCHEMA).toHaveProperty("author"); + expect(BLOG_POST_SCHEMA).toHaveProperty("date"); + expect(BLOG_POST_SCHEMA).toHaveProperty("tags"); + expect(BLOG_POST_SCHEMA).toHaveProperty("related"); + }); + + it("should have correct required field configuration", () => { + expect(BLOG_POST_SCHEMA.title.required).toBe(true); + expect(BLOG_POST_SCHEMA.description.required).toBe(true); + expect(BLOG_POST_SCHEMA.author.required).toBe(true); + expect(BLOG_POST_SCHEMA.date.required).toBe(true); + expect(BLOG_POST_SCHEMA.tags.required).toBe(false); + expect(BLOG_POST_SCHEMA.related.required).toBe(false); + }); + }); +}); From b54ddb16ba38851dbd0fe97892b83821f0957a6c Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:49:48 -0600 Subject: [PATCH 02/48] Added content processing system --- lib/cache.js | 240 ++++++++++++++++++ lib/content.js | 287 +++++++++++++++------ lib/contentProcessor.js | 376 ++++++++++++++++++++++++++++ lib/mdx.js | 262 +++++++++++++++++++ tests/unit/contentProcessor.test.js | 104 ++++++++ 5 files changed, 1192 insertions(+), 77 deletions(-) create mode 100644 lib/cache.js create mode 100644 lib/contentProcessor.js create mode 100644 lib/mdx.js create mode 100644 tests/unit/contentProcessor.test.js diff --git a/lib/cache.js b/lib/cache.js new file mode 100644 index 0000000..fff6558 --- /dev/null +++ b/lib/cache.js @@ -0,0 +1,240 @@ +/** + * Content caching utilities for improved performance + */ + +// In-memory cache for blog posts +const blogPostCache = new Map(); +const blogListCache = new Map(); +const tagCache = new Map(); +const authorCache = new Map(); + +// Cache configuration +const CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds +const MAX_CACHE_SIZE = 100; // Maximum number of cached items + +/** + * Cache entry with timestamp + */ +class CacheEntry { + constructor(data) { + this.data = data; + this.timestamp = Date.now(); + } + + isExpired() { + return Date.now() - this.timestamp > CACHE_TTL; + } +} + +/** + * Get cached blog post data + * @param {string} key - Cache key + * @returns {Object|null} Cached data or null if not found/expired + */ +function getCached(key) { + const entry = blogPostCache.get(key); + if (!entry || entry.isExpired()) { + blogPostCache.delete(key); + return null; + } + return entry.data; +} + +/** + * Set cached blog post data + * @param {string} key - Cache key + * @param {Object} data - Data to cache + */ +function setCached(key, data) { + // Implement LRU eviction if cache is full + if (blogPostCache.size >= MAX_CACHE_SIZE) { + const oldestKey = blogPostCache.keys().next().value; + blogPostCache.delete(oldestKey); + } + + blogPostCache.set(key, new CacheEntry(data)); +} + +/** + * Clear expired cache entries + */ +function clearExpiredCache() { + for (const [key, entry] of blogPostCache.entries()) { + if (entry.isExpired()) { + blogPostCache.delete(key); + } + } +} + +/** + * Clear all caches + */ +export function clearAllCaches() { + blogPostCache.clear(); + blogListCache.clear(); + tagCache.clear(); + authorCache.clear(); +} + +/** + * Get cached blog post by slug + * @param {string} slug - Blog post slug + * @returns {Object|null} Cached blog post or null + */ +export function getCachedBlogPost(slug) { + return getCached(`post:${slug}`); +} + +/** + * Cache blog post data + * @param {string} slug - Blog post slug + * @param {Object} postData - Blog post data + */ +export function cacheBlogPost(slug, postData) { + setCached(`post:${slug}`, postData); +} + +/** + * Get cached blog post list + * @param {string} key - Cache key for list (e.g., 'all', 'recent', 'tag:governance') + * @returns {Array|null} Cached list or null + */ +export function getCachedBlogList(key) { + const entry = blogListCache.get(key); + if (!entry || entry.isExpired()) { + blogListCache.delete(key); + return null; + } + return entry.data; +} + +/** + * Cache blog post list + * @param {string} key - Cache key + * @param {Array} listData - List data to cache + */ +export function cacheBlogList(key, listData) { + blogListCache.set(key, new CacheEntry(listData)); +} + +/** + * Get cached tags + * @returns {Array|null} Cached tags or null + */ +export function getCachedTags() { + const entry = tagCache.get("all"); + if (!entry || entry.isExpired()) { + tagCache.delete("all"); + return null; + } + return entry.data; +} + +/** + * Cache tags + * @param {Array} tags - Tags to cache + */ +export function cacheTags(tags) { + tagCache.set("all", new CacheEntry(tags)); +} + +/** + * Get cached authors + * @returns {Array|null} Cached authors or null + */ +export function getCachedAuthors() { + const entry = authorCache.get("all"); + if (!entry || entry.isExpired()) { + authorCache.delete("all"); + return null; + } + return entry.data; +} + +/** + * Cache authors + * @param {Array} authors - Authors to cache + */ +export function cacheAuthors(authors) { + authorCache.set("all", new CacheEntry(authors)); +} + +/** + * Invalidate cache for a specific blog post + * @param {string} slug - Blog post slug + */ +export function invalidateBlogPostCache(slug) { + blogPostCache.delete(`post:${slug}`); + // Also invalidate list caches since they might contain this post + blogListCache.clear(); +} + +/** + * Invalidate all caches + */ +export function invalidateAllCaches() { + clearAllCaches(); +} + +/** + * Get cache statistics + * @returns {Object} Cache statistics + */ +export function getCacheStats() { + clearExpiredCache(); + + return { + blogPostCacheSize: blogPostCache.size, + blogListCacheSize: blogListCache.size, + tagCacheSize: tagCache.size, + authorCacheSize: authorCache.size, + totalCacheSize: + blogPostCache.size + blogListCache.size + tagCache.size + authorCacheSize, + maxCacheSize: MAX_CACHE_SIZE, + cacheTTL: CACHE_TTL, + }; +} + +/** + * Warm up cache with frequently accessed data + * @param {Function} getAllPosts - Function to get all blog posts + * @param {Function} getAllTags - Function to get all tags + */ +export async function warmCache(getAllPosts, getAllTags) { + try { + // Cache all blog posts + const allPosts = getAllPosts(); + cacheBlogList("all", allPosts); + + // Cache recent posts + const recentPosts = allPosts.slice(0, 5); + cacheBlogList("recent", recentPosts); + + // Cache tags + const tags = getAllTags(); + cacheTags(tags); + + // Cache individual posts (first 10) + allPosts.slice(0, 10).forEach((post) => { + cacheBlogPost(post.slug, post); + }); + + console.log("Cache warmed up successfully"); + } catch (error) { + console.error("Error warming up cache:", error); + } +} + +/** + * Check if cache is healthy + * @returns {boolean} True if cache is healthy + */ +export function isCacheHealthy() { + try { + clearExpiredCache(); + return blogPostCache.size < MAX_CACHE_SIZE; + } catch (error) { + console.error("Cache health check failed:", error); + return false; + } +} diff --git a/lib/content.js b/lib/content.js index 2c284c5..5f4678e 100644 --- a/lib/content.js +++ b/lib/content.js @@ -7,6 +7,54 @@ import { validateBlogPost, sanitizeBlogPost } from "./validation.js"; * Content processing utilities for blog posts */ +/** + * Convert markdown content to HTML with basic formatting + * @param {string} markdown - Raw markdown content + * @returns {string} HTML content + */ +function markdownToHtml(markdown) { + if (!markdown) return ""; + + return ( + markdown + // Headers + .replace(/^### (.*$)/gim, "
") + .replace(/^(?!<[h|u|li])(.*$)/gim, "
$1
") + // Clean up empty paragraphs + .replace(/<\/p>/g, "") + .replace(/
(.*?)<\/p>/g, (match, content) => { + return content.trim() ? match : ""; + }) + ); +} + +/** + * Generate a URL-friendly slug from a string + * @param {string} text - Text to convert to slug + * @returns {string} URL-friendly slug + */ +function generateSlug(text) { + return text + .toLowerCase() + .replace(/[^\w\s-]/g, "") // Remove special characters + .replace(/\s+/g, "-") // Replace spaces with hyphens + .replace(/-+/g, "-") // Replace multiple hyphens with single + .trim(); +} + /** * Get all blog post files from the content directory * @returns {Array} Array of file paths @@ -31,125 +79,210 @@ export function getBlogPostFiles() { * @returns {Object|null} Parsed blog post data or null if invalid */ export function parseBlogPost(filePath) { - try { - const fullPath = path.join(process.cwd(), "content/blog", filePath); - const fileContents = fs.readFileSync(fullPath, "utf8"); - const { data: frontmatter, content } = matter(fileContents); + const fullPath = path.join(process.cwd(), "content/blog", filePath); - // Validate frontmatter - const validation = validateBlogPost(frontmatter); - if (!validation.isValid) { - console.error(`Validation failed for ${filePath}:`, validation.errors); + try { + const fileContents = fs.readFileSync(fullPath, "utf8"); + const { data, content } = matter(fileContents); + + const validationResult = validateBlogPost(data); + if (!validationResult.isValid) { + console.error( + `Validation errors for ${filePath}:`, + validationResult.errors + ); return null; } - // Sanitize frontmatter - const sanitized = sanitizeBlogPost(frontmatter); - - // Generate slug from filename - const slug = filePath.replace(/\.(md|mdx)$/, ""); + const sanitizedFrontmatter = sanitizeBlogPost(data); + const slug = generateSlug(filePath.replace(/\.mdx?$/, "")); return { slug, - frontmatter: sanitized, + frontmatter: sanitizedFrontmatter, content, + htmlContent: markdownToHtml(content), filePath, + lastModified: fs.statSync(fullPath).mtime, }; } catch (error) { - console.error(`Error parsing blog post ${filePath}:`, error); + console.error(`Error parsing blog post file ${filePath}:`, error); return null; } } /** - * Get all blog posts with parsed data + * Get all blog posts, sorted by date * @returns {Array} Array of parsed blog post objects */ export function getAllBlogPosts() { - const files = getBlogPostFiles(); - const posts = files - .map((file) => parseBlogPost(file)) - .filter((post) => post !== null) + const fileNames = getBlogPostFiles(); + const allPosts = fileNames + .map((fileName) => parseBlogPost(fileName)) + .filter(Boolean) // Filter out nulls (invalid posts) .sort( (a, b) => new Date(b.frontmatter.date) - new Date(a.frontmatter.date) - ); - - return posts; + ); // Sort by date descending + return allPosts; } /** - * Get a single blog post by slug - * @param {string} slug - The post slug - * @returns {Object|null} Parsed blog post or null if not found + * Get a single blog post by its slug + * @param {string} slug - The slug of the blog post + * @returns {Object|null} The parsed blog post data or null if not found */ export function getBlogPostBySlug(slug) { - const files = getBlogPostFiles(); - const file = files.find((f) => f.replace(/\.(md|mdx)$/, "") === slug); - - if (!file) { - return null; - } - - return parseBlogPost(file); -} - -/** - * Get related blog posts - * @param {string} currentSlug - Current post slug - * @param {Array} relatedSlugs - Array of related post slugs - * @param {number} limit - Maximum number of related posts to return - * @returns {Array} Array of related blog posts - */ -export function getRelatedBlogPosts(currentSlug, relatedSlugs = [], limit = 3) { - if (!relatedSlugs || relatedSlugs.length === 0) { - // Fallback: get posts with similar tags or recent posts - const allPosts = getAllBlogPosts(); - return allPosts.filter((post) => post.slug !== currentSlug).slice(0, limit); - } - const allPosts = getAllBlogPosts(); - const related = allPosts - .filter((post) => relatedSlugs.includes(post.slug)) - .slice(0, limit); - - // If we don't have enough related posts, fill with recent ones - if (related.length < limit) { - const recent = allPosts - .filter( - (post) => post.slug !== currentSlug && !relatedSlugs.includes(post.slug) - ) - .slice(0, limit - related.length); - return [...related, ...recent]; - } - - return related; + return allPosts.find((post) => post.slug === slug) || null; } /** - * Get all unique tags from blog posts - * @returns {Array} Array of unique tags + * Get related blog posts based on provided slugs or fallback to recent posts. + * @param {string} currentPostSlug - The slug of the current post to exclude. + * @param {string[]} relatedSlugs - Array of slugs for explicitly related posts. + * @param {number} limit - Maximum number of related posts to return. + * @returns {Array} Array of related blog post objects. + */ +export function getRelatedBlogPosts( + currentPostSlug, + relatedSlugs = [], + limit = 3 +) { + const allPosts = getAllBlogPosts(); + const filteredPosts = allPosts.filter( + (post) => post.slug !== currentPostSlug + ); + + let related = []; + if (relatedSlugs && relatedSlugs.length > 0) { + related = relatedSlugs + .map((slug) => filteredPosts.find((post) => post.slug === slug)) + .filter(Boolean); // Filter out any related slugs that don't exist + } + + // If not enough related posts, or no related slugs provided, fill with recent posts + if (related.length < limit) { + const remainingSlots = limit - related.length; + const existingRelatedSlugs = new Set(related.map((p) => p.slug)); + const recentPosts = filteredPosts + .filter((post) => !existingRelatedSlugs.has(post.slug)) + .slice(0, remainingSlots); + related = [...related, ...recentPosts]; + } + + return related.slice(0, limit); +} + +/** + * Get all unique tags from all blog posts. + * @returns {string[]} Array of unique tags. */ export function getAllTags() { - const posts = getAllBlogPosts(); + const allPosts = getAllBlogPosts(); const tags = new Set(); - - posts.forEach((post) => { + allPosts.forEach((post) => { if (post.frontmatter.tags) { post.frontmatter.tags.forEach((tag) => tags.add(tag)); } }); - - return Array.from(tags).sort(); + return Array.from(tags); } /** - * Get blog posts by tag - * @param {string} tag - Tag to filter by - * @returns {Array} Array of blog posts with the specified tag + * Get blog posts filtered by a specific tag. + * @param {string} tag - The tag to filter by. + * @returns {Object[]} Array of blog post objects matching the tag. */ export function getBlogPostsByTag(tag) { - const posts = getAllBlogPosts(); - return posts.filter( + const allPosts = getAllBlogPosts(); + return allPosts.filter( (post) => post.frontmatter.tags && post.frontmatter.tags.includes(tag) ); } + +/** + * Search blog posts by text content + * @param {string} query - Search query + * @param {number} limit - Maximum number of results + * @returns {Object[]} Array of matching blog post objects + */ +export function searchBlogPosts(query, limit = 10) { + if (!query || query.trim() === "") return []; + + const searchTerm = query.toLowerCase().trim(); + const allPosts = getAllBlogPosts(); + + const results = allPosts.filter((post) => { + const titleMatch = post.frontmatter.title + .toLowerCase() + .includes(searchTerm); + const descriptionMatch = post.frontmatter.description + .toLowerCase() + .includes(searchTerm); + const contentMatch = post.content.toLowerCase().includes(searchTerm); + const tagMatch = post.frontmatter.tags?.some((tag) => + tag.toLowerCase().includes(searchTerm) + ); + + return titleMatch || descriptionMatch || contentMatch || tagMatch; + }); + + return results.slice(0, limit); +} + +/** + * Get blog posts by author + * @param {string} author - Author name to filter by + * @returns {Object[]} Array of blog post objects by the author + */ +export function getBlogPostsByAuthor(author) { + const allPosts = getAllBlogPosts(); + return allPosts.filter( + (post) => post.frontmatter.author.toLowerCase() === author.toLowerCase() + ); +} + +/** + * Get recent blog posts + * @param {number} limit - Maximum number of posts to return + * @returns {Object[]} Array of recent blog post objects + */ +export function getRecentBlogPosts(limit = 5) { + const allPosts = getAllBlogPosts(); + return allPosts.slice(0, limit); +} + +/** + * Get blog post statistics + * @returns {Object} Statistics about blog posts + */ +export function getBlogStats() { + const allPosts = getAllBlogPosts(); + const tags = getAllTags(); + + return { + totalPosts: allPosts.length, + totalTags: tags.length, + totalAuthors: new Set(allPosts.map((post) => post.frontmatter.author)).size, + dateRange: { + earliest: + allPosts.length > 0 + ? allPosts[allPosts.length - 1].frontmatter.date + : null, + latest: allPosts.length > 0 ? allPosts[0].frontmatter.date : null, + }, + averagePostsPerMonth: + allPosts.length > 0 + ? Math.round( + (allPosts.length / + Math.max( + 1, + (new Date(allPosts[0].frontmatter.date) - + new Date(allPosts[allPosts.length - 1].frontmatter.date)) / + (1000 * 60 * 60 * 24 * 30) + )) * + 10 + ) / 10 + : 0, + }; +} diff --git a/lib/contentProcessor.js b/lib/contentProcessor.js new file mode 100644 index 0000000..260345e --- /dev/null +++ b/lib/contentProcessor.js @@ -0,0 +1,376 @@ +/** + * Comprehensive content processing system for blog posts + */ + +import { + processMarkdown, + generateTableOfContents, + processFrontmatter, +} from "./mdx.js"; +import { validateBlogPost, sanitizeBlogPost } from "./validation.js"; +import { + getCachedBlogPost, + cacheBlogPost, + getCachedBlogList, + cacheBlogList, + getCachedTags, + cacheTags, + warmCache, +} from "./cache.js"; +import fs from "fs"; +import path from "path"; + +/** + * Main content processor class + */ +class ContentProcessor { + constructor() { + this.contentDirectory = path.join(process.cwd(), "content/blog"); + this.processedPosts = new Map(); + this.isInitialized = false; + } + + /** + * Initialize the content processor + */ + async initialize() { + if (this.isInitialized) return; + + try { + // Warm up cache + await warmCache( + () => this.getAllPosts(), + () => this.getAllTags() + ); + + this.isInitialized = true; + console.log("Content processor initialized successfully"); + } catch (error) { + console.error("Failed to initialize content processor:", error); + throw error; + } + } + + /** + * Get all blog post files + * @returns {Array} Array of file paths + */ + getBlogPostFiles() { + try { + const files = fs.readdirSync(this.contentDirectory); + return files.filter( + (file) => file.endsWith(".md") || file.endsWith(".mdx") + ); + } catch (error) { + console.error("Error reading blog content directory:", error); + return []; + } + } + + /** + * Process a single blog post file + * @param {string} filePath - Path to the markdown file + * @returns {Object|null} Processed blog post data or null if invalid + */ + processBlogPost(filePath) { + const fullPath = path.join(this.contentDirectory, filePath); + + try { + const fileContents = fs.readFileSync(fullPath, "utf8"); + const { data, content } = require("gray-matter")(fileContents); + + // Validate frontmatter + const validationResult = validateBlogPost(data); + if (!validationResult.isValid) { + console.error( + `Validation errors for ${filePath}:`, + validationResult.errors + ); + return null; + } + + // Sanitize frontmatter + const sanitizedFrontmatter = sanitizeBlogPost(data); + + // Process markdown content + const processedContent = processMarkdown(content); + + // Generate slug + const slug = this.generateSlug(filePath.replace(/\.mdx?$/, "")); + + // Get file stats + const stats = fs.statSync(fullPath); + + // Create processed post object + const processedPost = { + slug, + frontmatter: processFrontmatter(sanitizedFrontmatter), + content: processedContent.content, + htmlContent: processedContent.htmlContent, + wordCount: processedContent.wordCount, + readingTime: processedContent.readingTime, + headings: processedContent.headings, + links: processedContent.links, + images: processedContent.images, + tableOfContents: generateTableOfContents(processedContent.headings), + filePath, + lastModified: stats.mtime, + fileSize: stats.size, + metadata: { + processedAt: new Date(), + processorVersion: "1.0.0", + }, + }; + + // Cache the processed post + cacheBlogPost(slug, processedPost); + + return processedPost; + } catch (error) { + console.error(`Error processing blog post file ${filePath}:`, error); + return null; + } + } + + /** + * Get all blog posts with caching + * @returns {Array} Array of processed blog post objects + */ + getAllPosts() { + // Check cache first + const cached = getCachedBlogList("all"); + if (cached) return cached; + + const fileNames = this.getBlogPostFiles(); + const allPosts = fileNames + .map((fileName) => this.processBlogPost(fileName)) + .filter(Boolean) + .sort( + (a, b) => new Date(b.frontmatter.date) - new Date(a.frontmatter.date) + ); + + // Cache the result + cacheBlogList("all", allPosts); + + return allPosts; + } + + /** + * Get a single blog post by slug with caching + * @param {string} slug - The slug of the blog post + * @returns {Object|null} The processed blog post data or null if not found + */ + getBlogPostBySlug(slug) { + // Check cache first + const cached = getCachedBlogPost(slug); + if (cached) return cached; + + // If not in cache, find and process the post + const allPosts = this.getAllPosts(); + const post = allPosts.find((post) => post.slug === slug); + + if (post) { + cacheBlogPost(slug, post); + return post; + } + + return null; + } + + /** + * Get recent blog posts + * @param {number} limit - Maximum number of posts to return + * @returns {Array} Array of recent blog post objects + */ + getRecentPosts(limit = 5) { + const cacheKey = `recent:${limit}`; + const cached = getCachedBlogList(cacheKey); + if (cached) return cached; + + const allPosts = this.getAllPosts(); + const recentPosts = allPosts.slice(0, limit); + + cacheBlogList(cacheKey, recentPosts); + return recentPosts; + } + + /** + * Get blog posts by tag + * @param {string} tag - The tag to filter by + * @returns {Array} Array of blog post objects matching the tag + */ + getPostsByTag(tag) { + const cacheKey = `tag:${tag}`; + const cached = getCachedBlogList(cacheKey); + if (cached) return cached; + + const allPosts = this.getAllPosts(); + const taggedPosts = allPosts.filter( + (post) => post.frontmatter.tags && post.frontmatter.tags.includes(tag) + ); + + cacheBlogList(cacheKey, taggedPosts); + return taggedPosts; + } + + /** + * Get all unique tags with caching + * @returns {Array} Array of unique tags + */ + getAllTags() { + const cached = getCachedTags(); + if (cached) return cached; + + const allPosts = this.getAllPosts(); + const tags = new Set(); + allPosts.forEach((post) => { + if (post.frontmatter.tags) { + post.frontmatter.tags.forEach((tag) => tags.add(tag)); + } + }); + + const tagsArray = Array.from(tags).sort(); + cacheTags(tagsArray); + return tagsArray; + } + + /** + * Search blog posts + * @param {string} query - Search query + * @param {number} limit - Maximum number of results + * @returns {Array} Array of matching blog post objects + */ + searchPosts(query, limit = 10) { + if (!query || query.trim() === "") return []; + + const searchTerm = query.toLowerCase().trim(); + const allPosts = this.getAllPosts(); + + const results = allPosts.filter((post) => { + const titleMatch = post.frontmatter.title + .toLowerCase() + .includes(searchTerm); + const descriptionMatch = post.frontmatter.description + .toLowerCase() + .includes(searchTerm); + const contentMatch = post.content.toLowerCase().includes(searchTerm); + const tagMatch = post.frontmatter.tags?.some((tag) => + tag.toLowerCase().includes(searchTerm) + ); + + return titleMatch || descriptionMatch || contentMatch || tagMatch; + }); + + return results.slice(0, limit); + } + + /** + * Get blog statistics + * @returns {Object} Statistics about blog posts + */ + getBlogStats() { + const allPosts = this.getAllPosts(); + const tags = this.getAllTags(); + + return { + totalPosts: allPosts.length, + totalTags: tags.length, + totalAuthors: new Set( + allPosts.map((post) => post.frontmatter.author).size + ), + totalWords: allPosts.reduce((sum, post) => sum + post.wordCount, 0), + averageReadingTime: + allPosts.length > 0 + ? Math.round( + allPosts.reduce((sum, post) => sum + post.readingTime, 0) / + allPosts.length + ) + : 0, + dateRange: { + earliest: + allPosts.length > 0 + ? allPosts[allPosts.length - 1].frontmatter.date + : null, + latest: allPosts.length > 0 ? allPosts[0].frontmatter.date : null, + }, + averagePostsPerMonth: + allPosts.length > 0 + ? Math.round( + (allPosts.length / + Math.max( + 1, + (new Date(allPosts[0].frontmatter.date) - + new Date(allPosts[allPosts.length - 1].frontmatter.date)) / + (1000 * 60 * 60 * 24 * 30) + )) * + 10 + ) / 10 + : 0, + }; + } + + /** + * Generate a URL-friendly slug from a string + * @param {string} text - Text to convert to slug + * @returns {string} URL-friendly slug + */ + generateSlug(text) { + return text + .toLowerCase() + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .trim(); + } + + /** + * Refresh content (reprocess all posts) + * @returns {Array} Array of reprocessed blog post objects + */ + refreshContent() { + console.log("Refreshing content..."); + + // Clear processed posts cache + this.processedPosts.clear(); + + // Reprocess all posts + const allPosts = this.getAllPosts(); + + console.log(`Refreshed ${allPosts.length} blog posts`); + return allPosts; + } + + /** + * Get content processing status + * @returns {Object} Status information + */ + getStatus() { + return { + isInitialized: this.isInitialized, + totalFiles: this.getBlogPostFiles().length, + processedPosts: this.processedPosts.size, + contentDirectory: this.contentDirectory, + lastRefresh: new Date().toISOString(), + }; + } +} + +// Create and export singleton instance +const contentProcessor = new ContentProcessor(); + +// Export the instance and convenience functions +export { contentProcessor }; + +// Export convenience functions bound to the instance +export const getAllPosts = () => contentProcessor.getAllPosts(); +export const getBlogPostBySlug = (slug) => + contentProcessor.getBlogPostBySlug(slug); +export const getRecentPosts = (limit) => contentProcessor.getRecentPosts(limit); +export const getPostsByTag = (tag) => contentProcessor.getPostsByTag(tag); +export const getAllTags = () => contentProcessor.getAllTags(); +export const searchPosts = (query, limit) => + contentProcessor.searchPosts(query, limit); +export const getBlogStats = () => contentProcessor.getBlogStats(); +export const refreshContent = () => contentProcessor.refreshContent(); +export const getStatus = () => contentProcessor.getStatus(); +export const initialize = () => contentProcessor.initialize(); diff --git a/lib/mdx.js b/lib/mdx.js new file mode 100644 index 0000000..0178b25 --- /dev/null +++ b/lib/mdx.js @@ -0,0 +1,262 @@ +/** + * MDX processing utilities for enhanced markdown content + */ + +/** + * Process markdown content with enhanced features + * @param {string} markdown - Raw markdown content + * @returns {Object} Processed content with metadata + */ +export function processMarkdown(markdown) { + if (!markdown) { + return { + content: "", + htmlContent: "", + wordCount: 0, + readingTime: 0, + headings: [], + links: [], + images: [], + }; + } + + // Extract headings for table of contents + const headings = extractHeadings(markdown); + + // Extract links + const links = extractLinks(markdown); + + // Extract images + const images = extractImages(markdown); + + // Calculate word count and reading time + const wordCount = calculateWordCount(markdown); + const readingTime = calculateReadingTime(wordCount); + + return { + content: markdown, + htmlContent: markdownToHtml(markdown), + wordCount, + readingTime, + headings, + links, + images, + }; +} + +/** + * Extract all headings from markdown content + * @param {string} markdown - Raw markdown content + * @returns {Array} Array of heading objects with level, text, and id + */ +function extractHeadings(markdown) { + const headingRegex = /^(#{1,6})\s+(.+)$/gm; + const headings = []; + let match; + + while ((match = headingRegex.exec(markdown)) !== null) { + const level = match[1].length; + const text = match[2].trim(); + const id = generateHeadingId(text); + + headings.push({ + level, + text, + id, + line: markdown.substring(0, match.index).split("\n").length, + }); + } + + return headings; +} + +/** + * Extract all links from markdown content + * @param {string} markdown - Raw markdown content + * @returns {Array} Array of link objects + */ +function extractLinks(markdown) { + const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; + const links = []; + let match; + + while ((match = linkRegex.exec(markdown)) !== null) { + links.push({ + text: match[1], + url: match[2], + index: match.index, + }); + } + + return links; +} + +/** + * Extract all images from markdown content + * @param {string} markdown - Raw markdown content + * @returns {Array} Array of image objects + */ +function extractImages(markdown) { + const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; + const images = []; + let match; + + while ((match = imageRegex.exec(markdown)) !== null) { + images.push({ + alt: match[1], + src: match[2], + index: match.index, + }); + } + + return images; +} + +/** + * Generate a unique ID for a heading + * @param {string} text - Heading text + * @returns {string} Unique ID + */ +function generateHeadingId(text) { + return text + .toLowerCase() + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .trim(); +} + +/** + * Calculate word count from markdown content + * @param {string} markdown - Raw markdown content + * @returns {number} Word count + */ +function calculateWordCount(markdown) { + // Remove markdown syntax and count words + const cleanText = markdown + .replace(/[#*`~\[\]()]/g, "") // Remove markdown characters + .replace(/\n+/g, " ") // Replace newlines with spaces + .trim(); + + return cleanText.split(/\s+/).filter((word) => word.length > 0).length; +} + +/** + * Calculate estimated reading time + * @param {number} wordCount - Number of words + * @returns {number} Reading time in minutes + */ +function calculateReadingTime(wordCount) { + const wordsPerMinute = 200; // Average reading speed + return Math.ceil(wordCount / wordsPerMinute); +} + +/** + * Convert markdown to HTML with enhanced formatting + * @param {string} markdown - Raw markdown content + * @returns {string} HTML content + */ +function markdownToHtml(markdown) { + if (!markdown) return ""; + + return ( + markdown + // Headers with IDs + .replace(/^### (.*$)/gim, (match, text) => { + const id = generateHeadingId(text); + return `
$2'
+ )
+ .replace(/`([^`]+)`/g, "$1")
+ // Links
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
+ // Lists
+ .replace(/^\* (.*$)/gim, "") + // Horizontal rules + .replace(/^---$/gm, "$1
") + .replace(/^(?!<[h|u|li|blockquote|hr|pre])(.*$)/gim, "
$1
") + // Clean up empty paragraphs and fix list wrapping + .replace(/<\/p>/g, "") + .replace(/
(.*?)<\/p>/g, (match, content) => { + return content.trim() ? match : ""; + }) + .replace(/<\/ul>\s*
post - Blog post object with frontmatter
+ variant - "vertical" (default) or "horizontal"
+ className - Additional CSS classes
+ showTags - Show/hide tags (default: true)
+ showReadingTime - Show/hide reading time
+ (default: true)
+
+ {JSON.stringify(mockPost1, null, 2)}
+
+ + {post.frontmatter.description} +
+{post.frontmatter.description}
@@ -78,29 +83,25 @@ const ContentContainer = ({ post, width = "200px", variant = "vertical" }) => {variant - "vertical" (default) or "horizontal"
- className - Additional CSS classes
showTags - Show/hide tags (default: true)
- showReadingTime - Show/hide reading time
- (default: true)
+ variant - "vertical" (default) or "horizontal"
+ (for development/testing)
+ {post.frontmatter.description} +
+- {post.frontmatter.description} -
{/* Article Content */} -) * @param {string} markdown - Raw markdown content * @returns {string} HTML content */ function markdownToHtml(markdown) { if (!markdown) return ""; + // Normalize line endings + const GAP_TOKEN = "
$2'
+ (m, lang = "", code) =>
+ `${code}`
)
.replace(/`([^`]+)`/g, "$1")
- // Links
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
- // Lists
- .replace(/^\* (.*$)/gim, "") - // Horizontal rules - .replace(/^---$/gm, "$1
") - .replace(/^(?!<[h|u|li|blockquote|hr|pre])(.*$)/gim, "
$1
") - // Clean up empty paragraphs and fix list wrapping - .replace(/<\/p>/g, "") - .replace(/
(.*?)<\/p>/g, (match, content) => { - return content.trim() ? match : ""; + .replace(/^(>\s?.+)(\n(>\s?.+))*$/gim, (m) => { + const inner = m.replace(/^>\s?/gm, ""); + return `
`; }) - .replace(/<\/ul>\s*${inner.replace( + /\n{2,}/g, + "
" + )}
")
+ // 2) Convert single line breaks to
tags within paragraphs
+ .replace(/(?")
+ // 3) Wrap remaining bare lines that are not already block-level elements.
+ // (Also skip our GAP_TOKEN so we can turn it into gap paragraphs later.)
+ .replace(
+ /^(?!\s*<(h[1-6]|ul|ol|li|blockquote|hr|pre|code|table|img)\b)(?!\s*<\/)(?!\s*
$2
" + ) + + // Clean up truly empty paragraphs but keep gap paragraphs + .replace(/\s*<\/p>/g, "")
+
+ // Turn counted GAP tokens into explicit, styleable gap elements
+ .replace(
+ /