on e4dea95 help sign in
urbit.org/lib/lib.js
334 lines 9.7 kB history raw
1import remark from "remark";
2import html from "remark-html";
3import fs from "fs";
4import { join, parse } from "path";
5import matter from "gray-matter";
6import toml from "@iarna/toml";
8const directories = {
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"),
16};
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
23const options = {
24 engines: {
25 toml: toml.parse.bind(toml),
26 },
27 language: "toml",
28 delimiters: "+++",
29};
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);
36 const items = {};
38 // Ensure only the minimal needed data is exposed
39 fields.forEach((field) => {
40 if (field === "slug") {
41 items[field] = realSlug;
42 }
44 if (field === "content") {
45 items[field] = content;
46 }
48 if (data[field]) {
49 items[field] = data[field];
50 }
51 });
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, "") || "";
56 return items;
59export function getAllPosts(fields = [], key) {
60 const slugs = getPostSlugs(key);
61 const posts = slugs
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));
66 return posts;
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);
77 } else {
78 arrayOfFiles.push(join(dirPath, "/", file));
79 }
80 });
82 return arrayOfFiles;
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];
95 }
96 });
97 return resultPost;
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];
106 }
107 });
108 return resultPost;
111export function formatDate(d) {
112 // const wk = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
113 const mn = [
114 "January",
115 "February",
116 "March",
117 "April",
118 "May",
119 "June",
120 "July",
121 "August",
122 "September",
123 "October",
124 "November",
125 "December",
126 ];
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('/')
136// return crumbs
137// .filter(v => v !== '')
138// .map((crumb, index) => ({
139// name: capitalize(crumb),
140// key: crumb,
141// path: crumbs.slice(0, index+2).join('/'),
142// }))
143// }
145/**
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).
150 *
151 */
153// type PageTree =
154// { title: string,
155// pages: [{
156// title: string,
157// base: string,
158// slug: string,
159// weight: number,
160// }],
161// children: { [string]: PageTree },
162// }
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
173 return {
174 title: metadata.data.title,
175 pages: pages
176 .map((page) => {
177 // retrieve topmatter as JSON
178 const { content, data } = matter(
179 fs.readFileSync(join(path, page.name)),
180 options
181 );
183 return {
184 title: data.title,
185 base: 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",
190 }),
191 };
192 })
193 .sort((a, b) => a.weight - b.weight),
194 children: Object.fromEntries(
195 subdirs
196 .sort((a, b) => {
197 const aMetadata = matter(
198 fs.readFileSync(join(path, a.name, "_index.md")),
199 options
200 );
201 const bMetadata = matter(
202 fs.readFileSync(join(path, b.name, "_index.md")),
203 options
204 );
205 return aMetadata?.data?.weight - bMetadata?.data?.weight || 0;
206 })
207 .map((subdir) => [
208 subdir.name,
209 buildPageTree(join(path, subdir.name), ordering, content),
210 ])
211 ),
212 };
215export const getPage = (path) => {
216 try {
217 let fileContents = fs.readFileSync(`${path}.md`, "utf8");
218 if (fileContents) {
219 const { data, content } = matter(fileContents, options);
220 return { data, content };
221 }
222 } catch {
223 try {
224 let fileContents = fs.readFileSync(`${path}/_index.md`, "utf8");
225 if (fileContents) {
226 const { data, content } = matter(fileContents, options);
227 return { data, content };
228 }
229 } catch {
230 console.error("no md file for slug");
231 }
232 }
233};
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")
241 // Parse frontmatter
242 .map((path) =>
243 matter(fs.readFileSync(join(directories.grants, path)), options)
244 )
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];
248 }, [])
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)) {
252 return acc;
253 }
254 return [...acc, uniqueItem];
255 }, []);
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
266 .map((path) =>
267 matter(fs.readFileSync(join(directories.grants, path)), options)
268 )
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];
272 }, [])
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)) {
276 return acc;
277 }
278 return [...acc, uniqueItem];
279 }, []);
280 return uniqueTypes;
283// Returns a number which represents how many grants are open, or uncompleted
284export function getOpenGrantsCount() {
285 const paths = fs.readdirSync(directories.grants);
286 const count = paths
287 // Get only markdown files
288 .filter((path) => parse(path).ext === ".md")
289 // Parse frontmatter to JSON
290 .map((path) =>
291 matter(fs.readFileSync(join(directories.grants, path)), options)
292 )
293 // Count grants which are both uncompleted and not assigned
294 .reduce((acc, metadata) => {
295 if (
296 !metadata.data.extra.completed &&
297 metadata.data.extra.assignee === ""
298 ) {
299 return acc + 1;
300 }
301 return acc;
302 }, 0);
303 return count;
306export function getSimilarGrants(type, category, num) {
307 const bestMatches = getAllPosts(
308 ["title", "slug", "date", "description", "extra", "taxonomies"],
309 "grants"
310 )
311 .map((grant) => {
312 return {
313 grant: grant,
314 typeWeight: grant.taxonomies.grant_type.reduce((acc, _type) => {
315 if (type === _type) {
316 return acc + 1;
317 }
318 return acc;
319 }),
320 tagWeight: grant.taxonomies.grant_category.reduce((acc, _category) => {
321 if (category === _category) {
322 return acc + 1;
323 }
324 return acc;
325 }),
326 };
327 })
328 .sort((a, b) => a.typeWeight + a.tagWeight - b.typeWeight + b.tagWeight)
329 .map((wrappedGrant) => {
330 return wrappedGrant.grant;
331 });
333 return bestMatches.slice(0, num);