on e8ea80c help sign in
urbit.org/lib/lib.js
361 lines 11 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";
7import { DateTime } from "luxon";
9const directories = (dir) => {
10 switch (dir) {
11 case "blog":
12 return join(process.cwd(), "content/blog");
13 case "events":
14 return join(process.cwd(), "content/events");
15 case "docs":
16 return join(process.cwd(), "content/docs");
17 case "grants":
18 return join(process.cwd(), "content/grants");
19 case "media":
20 return join(process.cwd(), "content/media");
21 case "updates":
22 return join(process.cwd(), "content/updates");
23 case "/":
24 return join(process.cwd(), "content");
25 default:
26 return join(process.cwd(), `content/${dir}`);
27 }
28};
30export function getPostSlugs(key) {
31 const dir = fs.existsSync(directories(key))
32 ? fs.readdirSync(directories(key), { withFileTypes: true })
33 : [];
34 return dir
35 .filter((f) => f.isFile() && f.name !== "_index.md")
36 .map((f) => f.name);
39// NB Gavin: This could be much simpler if we converted everything to a more common standard, like YAML
40const options = {
41 engines: {
42 toml: toml.parse.bind(toml),
43 },
44 language: "toml",
45 delimiters: "+++",
46};
48export function getPostBySlug(slug, fields = [], key) {
49 const realSlug = slug.replace(/\.md$/, "");
50 const fullPath = join(directories(key), `${realSlug}.md`);
51 const fileContents = fs.readFileSync(fullPath, "utf8");
52 const { data, content } = matter(fileContents, options);
53 const items = {};
55 // Ensure only the minimal needed data is exposed
56 fields.forEach((field) => {
57 if (field === "slug") {
58 items[field] = realSlug;
59 }
61 if (field === "content") {
62 items[field] = content;
63 }
65 if (data[field]) {
66 items[field] = data[field];
67 }
68 });
70 return items;
73export function getAllPosts(fields = [], key, sort = "") {
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 DateTime.fromISO(post1.date) > DateTime.fromISO(post2.date)
81 ? -1
82 : 1;
83 } else if (sort === "weight") {
84 return post1.weight > post2.weight ? -1 : 1;
85 }
86 });
87 return posts;
90export function getAllEvents(fields = [], key) {
91 const slugs = getPostSlugs(key);
92 const posts = slugs
93 .map((slug) => getPostBySlug(slug, fields, key))
94 // sort posts by date in descending order
95 .sort((post1, post2) =>
96 DateTime.fromISO(post1.starts) > DateTime.fromISO(post2.starts) ? -1 : 1
97 );
98 return posts;
101function getAllPaths(dirPath, arrayOfFiles) {
102 let files = fs.readdirSync(dirPath);
104 arrayOfFiles = arrayOfFiles || [];
106 files.forEach(function (file) {
107 if (fs.statSync(dirPath + "/" + file).isDirectory()) {
108 arrayOfFiles = getAllPaths(dirPath + "/" + file, arrayOfFiles);
109 } else {
110 arrayOfFiles.push(join(dirPath, "/", file));
111 }
112 });
114 return arrayOfFiles;
117export function getDocsPaths() {
118 return getAllPaths(directories("docs"), []);
121export function getNextPost(slug, fields = [], key, sort = "date") {
122 let resultPost = null;
124 getAllPosts(fields, key, sort).forEach((post, index, array) => {
125 if (post.slug === slug) {
126 if (typeof array[index - 1] !== "undefined") {
127 resultPost = array[index - 1];
128 }
129 }
130 });
131 return resultPost;
134export function getPreviousPost(slug, fields = [], key, sort = "date") {
135 let resultPost = null;
137 getAllPosts(fields, key, sort).forEach((post, index, array) => {
138 if (post.slug === slug) {
139 if (typeof array[index + 1] !== "undefined") {
140 resultPost = array[index + 1];
141 }
142 }
143 });
144 return resultPost;
147export function formatDateWithTime(d) {
148 const date = DateTime.fromISO(d, {
149 setZone: true,
150 }).toLocaleString(DateTime.DATETIME_FULL);
151 return date;
154export function capitalize(str) {
155 return str.charAt(0).toUpperCase() + str.slice(1);
158// Takes an ISO 8601 date string
159// Returns a time relative to the IANA timezone and iso8601 date
160export function generateDisplayDate(iso8601, zone = "America/Los_Angeles") {
161 return DateTime.fromISO(iso8601, { zone });
164// Takes an ISO 8601 date string
165// Returns a time relative to user's current timezone
166export function generateRealtimeDate(iso8601) {
167 return DateTime.fromISO(iso8601).setZone("system");
170// Formats a DateTime object to something like "July 15, 2021, 5:00 PM"
171export function formatDateAndTime(dateTimeObject) {
172 return dateTimeObject.toLocaleString(DateTime.DATETIME_HUGE);
175// Formats a DateTime object to something like "5:00 PM"
176export function formatTime(dateTimeObject) {
177 const dt = dateTimeObject.toLocaleString(DateTime.TIME_SIMPLE);
178 return dt.replace(/:00/g, "");
181export function formatTimeZone(dateTimeObject) {
182 return dateTimeObject.offsetNameShort;
185// Formats a DateTime object to something like "Jul 15, 2021"
186export function formatDate(dateTimeObject) {
187 return dateTimeObject.toLocaleString(DateTime.DATE_FULL);
190// export function generateCrumbs(path) {
191// const crumbs = path.split('/')
192// return crumbs
193// .filter(v => v !== '')
194// .map((crumb, index) => ({
195// name: capitalize(crumb),
196// key: crumb,
197// path: crumbs.slice(0, index+2).join('/'),
198// }))
199// }
201/**
202 * buildPageTree is written to provide a recursive index of posts
203 * for nested directories for creating sidebars
204 * @param {string} path Absolute path to the subdirectory.
205 * @returns {Object.<string,string|Object<string,string[]|number>>} Title of the directory, child pages, and child directories (recursive structure).
206 *
207 */
209// type PageTree =
210// { title: string,
211// pages: [{
212// title: string,
213// base: string,
214// slug: string,
215// weight: number,
216// }],
217// children: { [string]: PageTree },
218// }
220export function buildPageTree(path, ordering = "", content = false) {
221 const metadata = matter(fs.readFileSync(join(path, "_index.md")), options);
222 // 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
223 const children = fs.readdirSync(path, { withFileTypes: true });
224 // get a list of files
225 const pages = children.filter((f) => f.isFile() && f.name !== "_index.md");
226 // get a list of folders
227 const subdirs = children.filter((f) => f.isDirectory());
228 // return a pagetree datastructure
229 return {
230 title: metadata.data.title,
231 pages: pages
232 .map((page) => {
233 // retrieve topmatter as JSON
234 const { content, data } = matter(
235 fs.readFileSync(join(path, page.name)),
236 options
237 );
239 return {
240 title: data.title,
241 base: page.name,
242 slug: page.name.replace(/.md$/, ""),
243 ...(ordering === "weight" && { weight: data?.weight ?? 0 }),
244 ...(ordering === "date" && {
245 date: data?.date ?? "2000-01-01T00:00:00.000Z",
246 }),
247 };
248 })
249 .sort((a, b) => a.weight - b.weight),
250 children: Object.fromEntries(
251 subdirs
252 .sort((a, b) => {
253 const aMetadata = matter(
254 fs.readFileSync(join(path, a.name, "_index.md")),
255 options
256 );
257 const bMetadata = matter(
258 fs.readFileSync(join(path, b.name, "_index.md")),
259 options
260 );
261 return aMetadata?.data?.weight - bMetadata?.data?.weight || 0;
262 })
263 .map((subdir) => [
264 subdir.name,
265 buildPageTree(join(path, subdir.name), ordering, content),
266 ])
267 ),
268 };
271export const getPage = (path) => {
272 try {
273 let fileContents = fs.readFileSync(`${path}.md`, "utf8");
274 if (fileContents) {
275 const { data, content } = matter(fileContents, options);
276 return { data, content };
277 }
278 } catch {
279 try {
280 let fileContents = fs.readFileSync(`${path}/_index.md`, "utf8");
281 if (fileContents) {
282 const { data, content } = matter(fileContents, options);
283 return { data, content };
284 }
285 } catch {
286 console.error("no md file for slug");
287 }
288 }
289};
291// Returns an array of unique grants categories
292export function getGrantsCategories() {
293 const paths = fs.readdirSync(directories("grants"));
294 const uniqueCategories = paths
295 // Get only markdown files
296 .filter((path) => parse(path).ext === ".md")
297 // Parse frontmatter
298 .map((path) =>
299 matter(fs.readFileSync(join(directories("grants"), path)), options)
300 )
301 // Each markdown file has an array of categories. Collapse these arrays into one array.
302 .reduce((acc, metadata) => {
303 return [...acc, ...metadata.data.taxonomies.grant_category];
304 }, [])
305 // Deduplicate single array of categories such that the output is an array where each category is represented only once.
306 .reduce((acc, uniqueItem) => {
307 if (acc.includes(uniqueItem)) {
308 return acc;
309 }
310 return [...acc, uniqueItem];
311 }, []);
312 return uniqueCategories;
315// Returns an array of unique grants types
316export function getGrantsTypes() {
317 const paths = fs.readdirSync(directories("grants"));
318 const uniqueTypes = paths
319 // Get only markdown files
320 .filter((path) => parse(path).ext === ".md")
321 // Parse frontmatter to JSON
322 .map((path) =>
323 matter(fs.readFileSync(join(directories("grants"), path)), options)
324 )
325 // Each markdown file has an array of types. Collapse these arrays into one array.
326 .reduce((acc, metadata) => {
327 return [...acc, ...metadata.data.taxonomies.grant_type];
328 }, [])
329 // Deduplicate single array of types such that the output is an array where each type is represented only once.
330 .reduce((acc, uniqueItem) => {
331 if (acc.includes(uniqueItem)) {
332 return acc;
333 }
334 return [...acc, uniqueItem];
335 }, []);
336 return uniqueTypes;
339// Returns a number which represents how many grants are open, or uncompleted
340export function getOpenGrantsCount() {
341 const paths = fs.readdirSync(directories("grants"));
342 const count = paths
343 // Get only markdown files
344 .filter((path) => parse(path).ext === ".md")
345 // Parse frontmatter to JSON
346 .map((path) =>
347 matter(fs.readFileSync(join(directories("grants"), path)), options)
348 )
349 // Count grants which are both uncompleted and not assigned
350 .reduce((acc, metadata) => {
351 if (
352 !metadata.data.extra.completed &&
353 !metadata.data.extra.canceled &&
354 metadata.data.extra.assignee === ""
355 ) {
356 return acc + 1;
357 }
358 return acc;
359 }, 0);
360 return count;