1import remark from "remark";
2import html from "remark-html";
4import { join, parse } from "path";
5import matter from "gray-matter";
6import toml from "@iarna/toml";
9 blog: join(process.cwd(), "content/blog"),
10 events: join(process.cwd(), "content/events"),
11 docs: join(process.cwd(), "content/docs"),
12 grants: join(process.cwd(), "content/grants"),
13 media: join(process.cwd(), "content/media"),
14 updates: join(process.cwd(), "content/updates"),
15 "/": join(process.cwd(), "content"),
18export function getPostSlugs(key) {
19 return fs.readdirSync(directories[key]);
22// NB Gavin: This could be much simpler if we converted everything to a more common standard, like YAML
25 toml: toml.parse.bind(toml),
31export function getPostBySlug(slug, fields = [], key) {
32 const realSlug = slug.replace(/\.md$/, "");
33 const fullPath = join(directories[key], `${realSlug}.md`);
34 const fileContents = fs.readFileSync(fullPath, "utf8");
35 const { data, content } = matter(fileContents, options);
38 // Ensure only the minimal needed data is exposed
39 fields.forEach((field) => {
40 if (field === "slug") {
41 items[field] = realSlug;
44 if (field === "content") {
45 items[field] = content;
49 items[field] = data[field];
53 // 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.
54 items.date = JSON.stringify(items?.date)?.replace(/\"/g, "") || "";
59export function getAllPosts(fields = [], key) {
60 const slugs = getPostSlugs(key);
62 .map((slug) => getPostBySlug(slug, fields, key))
63 // sort posts by date in descending order
64 .sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
69function getAllPaths(dirPath, arrayOfFiles) {
70 let files = fs.readdirSync(dirPath);
72 arrayOfFiles = arrayOfFiles || [];
74 files.forEach(function (file) {
75 if (fs.statSync(dirPath + "/" + file).isDirectory()) {
76 arrayOfFiles = getAllPaths(dirPath + "/" + file, arrayOfFiles);
78 arrayOfFiles.push(join(dirPath, "/", file));
85export function getDocsPaths() {
86 return getAllPaths(directories.docs, []);
89export function getNextPost(slug, fields = [], key) {
90 let resultPost = null;
92 getAllPosts(fields, key).forEach((post, index, array) => {
93 if (post.slug === slug) {
94 resultPost = array[index - 1];
100export function getPreviousPost(slug, fields = [], key) {
101 let resultPost = null;
103 getAllPosts(fields, key).forEach((post, index, array) => {
104 if (post.slug === slug) {
105 resultPost = array[index + 1];
111export function formatDate(d) {
112 // const wk = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
127 return mn[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear();
130export function capitalize(str) {
131 return str.charAt(0).toUpperCase() + str.slice(1);
134// export function generateCrumbs(path) {
135// const crumbs = path.split('/')
137// .filter(v => v !== '')
138// .map((crumb, index) => ({
139// name: capitalize(crumb),
141// path: crumbs.slice(0, index+2).join('/'),
146 * buildPageTree is written to provide a recursive index of posts
147 * for nested directories for creating sidebars
148 * @param {string} path Absolute path to the subdirectory.
149 * @returns {Object.<string,string|Object<string,string[]|number>>} Title of the directory, child pages, and child directories (recursive structure).
161// children: { [string]: PageTree },
164export function buildPageTree(path, ordering = "", content = false) {
165 const metadata = matter(fs.readFileSync(join(path, "_index.md")), options);
166 // 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
167 const children = fs.readdirSync(path, { withFileTypes: true });
168 // get a list of files
169 const pages = children.filter((f) => f.isFile() && f.name !== "_index.md");
170 // get a list of folders
171 const subdirs = children.filter((f) => f.isDirectory());
172 // return a pagetree datastructure
174 title: metadata.data.title,
177 // retrieve topmatter as JSON
178 const { content, data } = matter(
179 fs.readFileSync(join(path, page.name)),
186 slug: page.name.replace(/.md$/, ""),
187 ...(ordering === "weight" && { weight: data?.weight ?? 0 }),
188 ...(ordering === "date" && {
189 date: data?.date ?? "2000-01-01T00:00:00.000Z",
193 .sort((a, b) => a.weight - b.weight),
194 children: Object.fromEntries(
197 const aMetadata = matter(
198 fs.readFileSync(join(path, a.name, "_index.md")),
201 const bMetadata = matter(
202 fs.readFileSync(join(path, b.name, "_index.md")),
205 return aMetadata?.data?.weight - bMetadata?.data?.weight || 0;
209 buildPageTree(join(path, subdir.name), ordering, content),
215export const getPage = (path) => {
217 let fileContents = fs.readFileSync(`${path}.md`, "utf8");
219 const { data, content } = matter(fileContents, options);
220 return { data, content };
224 let fileContents = fs.readFileSync(`${path}/_index.md`, "utf8");
226 const { data, content } = matter(fileContents, options);
227 return { data, content };
230 console.error("no md file for slug");
235// Returns an array of unique grants categories
236export function getGrantsCategories() {
237 const paths = fs.readdirSync(directories.grants);
238 const uniqueCategories = paths
239 // Get only markdown files
240 .filter((path) => parse(path).ext === ".md")
243 matter(fs.readFileSync(join(directories.grants, path)), options)
245 // Each markdown file has an array of categories. Collapse these arrays into one array.
246 .reduce((acc, metadata) => {
247 return [...acc, ...metadata.data.taxonomies.grant_category];
249 // Deduplicate single array of categories such that the output is an array where each category is represented only once.
250 .reduce((acc, uniqueItem) => {
251 if (acc.includes(uniqueItem)) {
254 return [...acc, uniqueItem];
256 return uniqueCategories;
259// Returns an array of unique grants types
260export function getGrantsTypes() {
261 const paths = fs.readdirSync(directories.grants);
262 const uniqueTypes = paths
263 // Get only markdown files
264 .filter((path) => parse(path).ext === ".md")
265 // Parse frontmatter to JSON
267 matter(fs.readFileSync(join(directories.grants, path)), options)
269 // Each markdown file has an array of types. Collapse these arrays into one array.
270 .reduce((acc, metadata) => {
271 return [...acc, ...metadata.data.taxonomies.grant_type];
273 // Deduplicate single array of types such that the output is an array where each type is represented only once.
274 .reduce((acc, uniqueItem) => {
275 if (acc.includes(uniqueItem)) {
278 return [...acc, uniqueItem];
283// Returns a number which represents how many grants are open, or uncompleted
284export function getOpenGrantsCount() {
285 const paths = fs.readdirSync(directories.grants);
287 // Get only markdown files
288 .filter((path) => parse(path).ext === ".md")
289 // Parse frontmatter to JSON
291 matter(fs.readFileSync(join(directories.grants, path)), options)
293 // Count grants which are both uncompleted and not assigned
294 .reduce((acc, metadata) => {
296 !metadata.data.extra.completed &&
297 metadata.data.extra.assignee === ""
306export function getSimilarGrants(type, category, num) {
307 const bestMatches = getAllPosts(
308 ["title", "slug", "date", "description", "extra", "taxonomies"],
314 typeWeight: grant.taxonomies.grant_type.reduce((acc, _type) => {
315 if (type === _type) {
320 tagWeight: grant.taxonomies.grant_category.reduce((acc, _category) => {
321 if (category === _category) {
328 .sort((a, b) => a.typeWeight + a.tagWeight - b.typeWeight + b.tagWeight)
329 .map((wrappedGrant) => {
330 return wrappedGrant.grant;
333 return bestMatches.slice(0, num);
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list