on 40fbeae help sign in
urbit.org/lib/lib.js
354 lines 10 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 = (dir) => {
9 switch (dir) {
10 case "blog":
11 return join(process.cwd(), "content/blog");
12 case "events":
13 return join(process.cwd(), "content/events");
14 case "docs":
15 return join(process.cwd(), "content/docs");
16 case "grants":
17 return join(process.cwd(), "content/grants");
18 case "media":
19 return join(process.cwd(), "content/media");
20 case "updates":
21 return join(process.cwd(), "content/updates");
22 case "/":
23 return join(process.cwd(), "content");
24 default:
25 return join(process.cwd(), `content/${dir}`);
26 }
27};
29export function getPostSlugs(key) {
30 const dir = fs.readdirSync(directories(key), { withFileTypes: true });
31 return dir
32 .filter((f) => f.isFile() && f.name !== "_index.md")
33 .map((f) => f.name);
36// NB Gavin: This could be much simpler if we converted everything to a more common standard, like YAML
37const options = {
38 engines: {
39 toml: toml.parse.bind(toml),
40 },
41 language: "toml",
42 delimiters: "+++",
43};
45export function getPostBySlug(slug, fields = [], key) {
46 const realSlug = slug.replace(/\.md$/, "");
47 const fullPath = join(directories(key), `${realSlug}.md`);
48 const fileContents = fs.readFileSync(fullPath, "utf8");
49 const { data, content } = matter(fileContents, options);
50 const items = {};
52 // Ensure only the minimal needed data is exposed
53 fields.forEach((field) => {
54 if (field === "slug") {
55 items[field] = realSlug;
56 }
58 if (field === "content") {
59 items[field] = content;
60 }
62 if (data[field]) {
63 items[field] = data[field];
64 }
65 });
67 // 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.
68 items.date = JSON.stringify(items?.date)?.replace(/\"/g, "") || "";
70 return items;
73export function getAllPosts(fields = [], key, sort = "date") {
74 const slugs = getPostSlugs(key);
75 const posts = slugs
76 .map((slug) => getPostBySlug(slug, fields, key))
77 // sort posts by date in descending order
78 .sort((post1, post2) => {
79 if (sort === "date") {
80 return post1.date > post2.date ? -1 : 1;
81 } else if (sort === "weight") {
82 return post1.weight > post2.weight ? -1 : 1;
83 }
84 });
86 return posts;
89function getAllPaths(dirPath, arrayOfFiles) {
90 let files = fs.readdirSync(dirPath);
92 arrayOfFiles = arrayOfFiles || [];
94 files.forEach(function (file) {
95 if (fs.statSync(dirPath + "/" + file).isDirectory()) {
96 arrayOfFiles = getAllPaths(dirPath + "/" + file, arrayOfFiles);
97 } else {
98 arrayOfFiles.push(join(dirPath, "/", file));
99 }
100 });
102 return arrayOfFiles;
105export function getDocsPaths() {
106 return getAllPaths(directories("docs"), []);
109export function getNextPost(slug, fields = [], key, sort = "date") {
110 let resultPost = null;
112 getAllPosts(fields, key, sort).forEach((post, index, array) => {
113 if (post.slug === slug) {
114 resultPost = array[index - 1];
115 }
116 });
117 return resultPost;
120export function getPreviousPost(slug, fields = [], key, sort = "date") {
121 let resultPost = null;
123 getAllPosts(fields, key, sort).forEach((post, index, array) => {
124 if (post.slug === slug) {
125 resultPost = array[index + 1];
126 }
127 });
128 return resultPost;
131export function formatDate(d) {
132 // const wk = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
133 const mn = [
134 "January",
135 "February",
136 "March",
137 "April",
138 "May",
139 "June",
140 "July",
141 "August",
142 "September",
143 "October",
144 "November",
145 "December",
146 ];
147 return mn[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear();
150export function capitalize(str) {
151 return str.charAt(0).toUpperCase() + str.slice(1);
154// export function generateCrumbs(path) {
155// const crumbs = path.split('/')
156// return crumbs
157// .filter(v => v !== '')
158// .map((crumb, index) => ({
159// name: capitalize(crumb),
160// key: crumb,
161// path: crumbs.slice(0, index+2).join('/'),
162// }))
163// }
165/**
166 * buildPageTree is written to provide a recursive index of posts
167 * for nested directories for creating sidebars
168 * @param {string} path Absolute path to the subdirectory.
169 * @returns {Object.<string,string|Object<string,string[]|number>>} Title of the directory, child pages, and child directories (recursive structure).
170 *
171 */
173// type PageTree =
174// { title: string,
175// pages: [{
176// title: string,
177// base: string,
178// slug: string,
179// weight: number,
180// }],
181// children: { [string]: PageTree },
182// }
184export function buildPageTree(path, ordering = "", content = false) {
185 const metadata = matter(fs.readFileSync(join(path, "_index.md")), options);
186 // 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
187 const children = fs.readdirSync(path, { withFileTypes: true });
188 // get a list of files
189 const pages = children.filter((f) => f.isFile() && f.name !== "_index.md");
190 // get a list of folders
191 const subdirs = children.filter((f) => f.isDirectory());
192 // return a pagetree datastructure
193 return {
194 title: metadata.data.title,
195 pages: pages
196 .map((page) => {
197 // retrieve topmatter as JSON
198 const { content, data } = matter(
199 fs.readFileSync(join(path, page.name)),
200 options
201 );
203 return {
204 title: data.title,
205 base: page.name,
206 slug: page.name.replace(/.md$/, ""),
207 ...(ordering === "weight" && { weight: data?.weight ?? 0 }),
208 ...(ordering === "date" && {
209 date: data?.date ?? "2000-01-01T00:00:00.000Z",
210 }),
211 };
212 })
213 .sort((a, b) => a.weight - b.weight),
214 children: Object.fromEntries(
215 subdirs
216 .sort((a, b) => {
217 const aMetadata = matter(
218 fs.readFileSync(join(path, a.name, "_index.md")),
219 options
220 );
221 const bMetadata = matter(
222 fs.readFileSync(join(path, b.name, "_index.md")),
223 options
224 );
225 return aMetadata?.data?.weight - bMetadata?.data?.weight || 0;
226 })
227 .map((subdir) => [
228 subdir.name,
229 buildPageTree(join(path, subdir.name), ordering, content),
230 ])
231 ),
232 };
235export const getPage = (path) => {
236 try {
237 let fileContents = fs.readFileSync(`${path}.md`, "utf8");
238 if (fileContents) {
239 const { data, content } = matter(fileContents, options);
240 return { data, content };
241 }
242 } catch {
243 try {
244 let fileContents = fs.readFileSync(`${path}/_index.md`, "utf8");
245 if (fileContents) {
246 const { data, content } = matter(fileContents, options);
247 return { data, content };
248 }
249 } catch {
250 console.error("no md file for slug");
251 }
252 }
253};
255// Returns an array of unique grants categories
256export function getGrantsCategories() {
257 const paths = fs.readdirSync(directories("grants"));
258 const uniqueCategories = paths
259 // Get only markdown files
260 .filter((path) => parse(path).ext === ".md")
261 // Parse frontmatter
262 .map((path) =>
263 matter(fs.readFileSync(join(directories("grants"), path)), options)
264 )
265 // Each markdown file has an array of categories. Collapse these arrays into one array.
266 .reduce((acc, metadata) => {
267 return [...acc, ...metadata.data.taxonomies.grant_category];
268 }, [])
269 // Deduplicate single array of categories such that the output is an array where each category is represented only once.
270 .reduce((acc, uniqueItem) => {
271 if (acc.includes(uniqueItem)) {
272 return acc;
273 }
274 return [...acc, uniqueItem];
275 }, []);
276 return uniqueCategories;
279// Returns an array of unique grants types
280export function getGrantsTypes() {
281 const paths = fs.readdirSync(directories("grants"));
282 const uniqueTypes = paths
283 // Get only markdown files
284 .filter((path) => parse(path).ext === ".md")
285 // Parse frontmatter to JSON
286 .map((path) =>
287 matter(fs.readFileSync(join(directories("grants"), path)), options)
288 )
289 // Each markdown file has an array of types. Collapse these arrays into one array.
290 .reduce((acc, metadata) => {
291 return [...acc, ...metadata.data.taxonomies.grant_type];
292 }, [])
293 // Deduplicate single array of types such that the output is an array where each type is represented only once.
294 .reduce((acc, uniqueItem) => {
295 if (acc.includes(uniqueItem)) {
296 return acc;
297 }
298 return [...acc, uniqueItem];
299 }, []);
300 return uniqueTypes;
303// Returns a number which represents how many grants are open, or uncompleted
304export function getOpenGrantsCount() {
305 const paths = fs.readdirSync(directories("grants"));
306 const count = paths
307 // Get only markdown files
308 .filter((path) => parse(path).ext === ".md")
309 // Parse frontmatter to JSON
310 .map((path) =>
311 matter(fs.readFileSync(join(directories("grants"), path)), options)
312 )
313 // Count grants which are both uncompleted and not assigned
314 .reduce((acc, metadata) => {
315 if (
316 !metadata.data.extra.completed &&
317 metadata.data.extra.assignee === ""
318 ) {
319 return acc + 1;
320 }
321 return acc;
322 }, 0);
323 return count;
326export function getSimilarGrants(type, category, num) {
327 const bestMatches = getAllPosts(
328 ["title", "slug", "date", "description", "extra", "taxonomies"],
329 "grants"
330 )
331 .map((grant) => {
332 return {
333 grant: grant,
334 typeWeight: grant.taxonomies.grant_type.reduce((acc, _type) => {
335 if (type === _type) {
336 return acc + 1;
337 }
338 return acc;
339 }),
340 tagWeight: grant.taxonomies.grant_category.reduce((acc, _category) => {
341 if (category === _category) {
342 return acc + 1;
343 }
344 return acc;
345 }),
346 };
347 })
348 .sort((a, b) => a.typeWeight + a.tagWeight - b.typeWeight + b.tagWeight)
349 .map((wrappedGrant) => {
350 return wrappedGrant.grant;
351 });
353 return bestMatches.slice(0, num);