import remark from "remark"; import html from "remark-html"; import fs from "fs"; import { join, parse } from "path"; import matter from "gray-matter"; import toml from "@iarna/toml"; const directories = { blog: join(process.cwd(), "content/blog"), events: join(process.cwd(), "content/events"), docs: join(process.cwd(), "content/docs"), grants: join(process.cwd(), "content/grants"), media: join(process.cwd(), "content/media"), }; export function getPostSlugs(key) { return fs.readdirSync(directories[key]); } // NB Gavin: This could be much simpler if we converted everything to a more common standard, like YAML const options = { engines: { toml: toml.parse.bind(toml), }, language: "toml", delimiters: "+++", }; export function getPostBySlug(slug, fields = [], key) { const realSlug = slug.replace(/\.md$/, ""); const fullPath = join(directories[key], `${realSlug}.md`); const fileContents = fs.readFileSync(fullPath, "utf8"); const { data, content } = matter(fileContents, options); const items = {}; // Ensure only the minimal needed data is exposed fields.forEach((field) => { if (field === "slug") { items[field] = realSlug; } if (field === "content") { items[field] = content; } if (data[field]) { items[field] = data[field]; } }); // NB Gavin: This could be removed if the TOML engine returned a JSON date (string) instead of a JS Date object. The YAML engine did this parsing by default. items.date = JSON.stringify(items.date).replace(/\"/g, ""); return items; } export function getAllPosts(fields = [], key) { const slugs = getPostSlugs(key); const posts = slugs .map((slug) => getPostBySlug(slug, fields, key)) // sort posts by date in descending order .sort((post1, post2) => (post1.date > post2.date ? -1 : 1)); return posts; } function getAllPaths(dirPath, arrayOfFiles) { let files = fs.readdirSync(dirPath); arrayOfFiles = arrayOfFiles || []; files.forEach(function (file) { if (fs.statSync(dirPath + "/" + file).isDirectory()) { arrayOfFiles = getAllPaths(dirPath + "/" + file, arrayOfFiles); } else { arrayOfFiles.push(join(dirPath, "/", file)); } }); return arrayOfFiles; } export function getDocsPaths() { return getAllPaths(directories.docs, []); } export function getNextPost(slug, fields = [], key) { let resultPost = null; getAllPosts(fields, key).forEach((post, index, array) => { if (post.slug === slug) { resultPost = array[index - 1]; } }); return resultPost; } export function getPreviousPost(slug, fields = [], key) { let resultPost = null; getAllPosts(fields, key).forEach((post, index, array) => { if (post.slug === slug) { resultPost = array[index + 1]; } }); return resultPost; } export function formatDate(d) { // const wk = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] const mn = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; return mn[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear(); } // export function capitalize(str) { // return str.charAt(0).toUpperCase() + str.slice(1); // } // export function generateCrumbs(path) { // const crumbs = path.split('/') // return crumbs // .filter(v => v !== '') // .map((crumb, index) => ({ // name: capitalize(crumb), // key: crumb, // path: crumbs.slice(0, index+2).join('/'), // })) // } /** * buildPageTree is written to provide a recursive index of posts * for nested directories for creating sidebars * @param {string} path Absolute path to the subdirectory. * @returns {Object.>} Title of the directory, child pages, and child directories (recursive structure). * */ // type PageTree = // { title: string, // pages: [{ // title: string, // base: string, // slug: string, // weight: number, // }], // children: { [string]: PageTree }, // } export function buildPageTree(path, ordering = "", content = false) { // get metadata from _index.md file in each folder. This is where page ordering (weight) is stored. const metadata = matter(fs.readFileSync(join(path, "_index.md")), options); // get a list of contents at the path, with file types so that the item at the path can be identified as file or folder const children = fs.readdirSync(path, { withFileTypes: true }); // get a list of files const pages = children.filter((f) => f.isFile() && f.name !== "_index.md"); // get a list of folders const subdirs = children.filter((f) => f.isDirectory()); // return a pagetree datastructure return { title: metadata.data.title, pages: pages .map((page) => { // retrieve topmatter as JSON const { content, data } = matter( fs.readFileSync(join(path, page.name)), options ); return { title: data.title, base: page.name, slug: page.name.replace(/.md$/, ""), ...(ordering === "weight" && { weight: data?.weight ?? 0 }), ...(ordering === "date" && { date: data?.date ?? "2000-01-01T00:00:00.000Z", }), }; }) // Sort files according to weight. Weight can be a date or number. The choice between the two is set by the 'ordering' param. .sort((a, b) => a.weight - b.weight), children: Object.fromEntries( subdirs.map((subdir) => [ subdir.name, buildPageTree(join(path, subdir.name), ordering, content), ]) ), }; } export const getPage = (path) => { try { let fileContents = fs.readFileSync(`${path}.md`, "utf8"); if (fileContents) { const { data, content } = matter(fileContents, options); return { data, content }; } } catch { try { let fileContents = fs.readFileSync(`${path}/_index.md`, "utf8"); if (fileContents) { const { data, content } = matter(fileContents, options); return { data, content }; } } catch { console.error("no md file for slug"); } } }; // Returns an array of unique grants categories export function getGrantsCategories() { const paths = fs.readdirSync(directories.grants); const uniqueCategories = paths // Get only markdown files .filter((path) => parse(path).ext === ".md") // Parse frontmatter .map((path) => matter(fs.readFileSync(join(directories.grants, path)), options) ) // Each markdown file has an array of categories. Collapse these arrays into one array. .reduce((acc, metadata) => { return [...acc, ...metadata.data.taxonomies.grant_category]; }, []) // Deduplicate single array of categories such that the output is an array where each category is represented only once. .reduce((acc, uniqueItem) => { if (acc.includes(uniqueItem)) { return acc; } return [...acc, uniqueItem]; }, []); return uniqueCategories; } // Returns an array of unique grants types export function getGrantsTypes() { const paths = fs.readdirSync(directories.grants); const uniqueTypes = paths // Get only markdown files .filter((path) => parse(path).ext === ".md") // Parse frontmatter to JSON .map((path) => matter(fs.readFileSync(join(directories.grants, path)), options) ) // Each markdown file has an array of types. Collapse these arrays into one array. .reduce((acc, metadata) => { return [...acc, ...metadata.data.taxonomies.grant_type]; }, []) // Deduplicate single array of types such that the output is an array where each type is represented only once. .reduce((acc, uniqueItem) => { if (acc.includes(uniqueItem)) { return acc; } return [...acc, uniqueItem]; }, []); return uniqueTypes; } // Returns a number which represents how many grants are open, or uncompleted export function getOpenGrantsCount() { const paths = fs.readdirSync(directories.grants); const count = paths // Get only markdown files .filter((path) => parse(path).ext === ".md") // Parse frontmatter to JSON .map((path) => matter(fs.readFileSync(join(directories.grants, path)), options) ) // Count non-completed grants .reduce((acc, metadata) => { if (metadata.data.extra.completed) { return acc; } return acc + 1; }, 0); return count; }