on roller-tutorial help sign in
urbit.org/lib/lib.js
359 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.readdirSync(directories(key), { withFileTypes: true });
32 return dir
33 .filter((f) => f.isFile() && f.name !== "_index.md")
34 .map((f) => f.name);
37// NB Gavin: This could be much simpler if we converted everything to a more common standard, like YAML
38const options = {
39 engines: {
40 toml: toml.parse.bind(toml),
41 },
42 language: "toml",
43 delimiters: "+++",
44};
46export function getPostBySlug(slug, fields = [], key) {
47 const realSlug = slug.replace(/\.md$/, "");
48 const fullPath = join(directories(key), `${realSlug}.md`);
49 const fileContents = fs.readFileSync(fullPath, "utf8");
50 const { data, content } = matter(fileContents, options);
51 const items = {};
53 // Ensure only the minimal needed data is exposed
54 fields.forEach((field) => {
55 if (field === "slug") {
56 items[field] = realSlug;
57 }
59 if (field === "content") {
60 items[field] = content;
61 }
63 if (data[field]) {
64 items[field] = data[field];
65 }
66 });
68 return items;
71export function getAllPosts(fields = [], key, sort = "date") {
72 const slugs = getPostSlugs(key);
73 const posts = slugs
74 .map((slug) => getPostBySlug(slug, fields, key))
75 // sort posts by date in descending order
76 .sort((post1, post2) => {
77 if (sort === "date") {
78 return DateTime.fromISO(post1.date) > DateTime.fromISO(post2.date)
79 ? -1
80 : 1;
81 } else if (sort === "weight") {
82 return post1.weight > post2.weight ? -1 : 1;
83 }
84 });
85 return posts;
88export function getAllEvents(fields = [], key) {
89 const slugs = getPostSlugs(key);
90 const posts = slugs
91 .map((slug) => getPostBySlug(slug, fields, key))
92 // sort posts by date in descending order
93 .sort((post1, post2) =>
94 DateTime.fromISO(post1.starts) > DateTime.fromISO(post2.starts) ? -1 : 1
95 );
96 return posts;
99function getAllPaths(dirPath, arrayOfFiles) {
100 let files = fs.readdirSync(dirPath);
102 arrayOfFiles = arrayOfFiles || [];
104 files.forEach(function (file) {
105 if (fs.statSync(dirPath + "/" + file).isDirectory()) {
106 arrayOfFiles = getAllPaths(dirPath + "/" + file, arrayOfFiles);
107 } else {
108 arrayOfFiles.push(join(dirPath, "/", file));
109 }
110 });
112 return arrayOfFiles;
115export function getDocsPaths() {
116 return getAllPaths(directories("docs"), []);
119export function getNextPost(slug, fields = [], key, sort = "date") {
120 let resultPost = null;
122 getAllPosts(fields, key, sort).forEach((post, index, array) => {
123 if (post.slug === slug) {
124 if (typeof array[index - 1] !== "undefined") {
125 resultPost = array[index - 1];
126 }
127 }
128 });
129 return resultPost;
132export function getPreviousPost(slug, fields = [], key, sort = "date") {
133 let resultPost = null;
135 getAllPosts(fields, key, sort).forEach((post, index, array) => {
136 if (post.slug === slug) {
137 if (typeof array[index + 1] !== "undefined") {
138 resultPost = array[index + 1];
139 }
140 }
141 });
142 return resultPost;
145export function formatDateWithTime(d) {
146 const date = DateTime.fromISO(d, {
147 setZone: true,
148 }).toLocaleString(DateTime.DATETIME_FULL);
149 return date;
152export function capitalize(str) {
153 return str.charAt(0).toUpperCase() + str.slice(1);
156// Takes an ISO 8601 date string
157// Returns a time relative to the IANA timezone and iso8601 date
158export function generateDisplayDate(iso8601, zone = "America/Los_Angeles") {
159 return DateTime.fromISO(iso8601, { zone });
162// Takes an ISO 8601 date string
163// Returns a time relative to user's current timezone
164export function generateRealtimeDate(iso8601) {
165 return DateTime.fromISO(iso8601).setZone("system");
168// Formats a DateTime object to something like "July 15, 2021, 5:00 PM"
169export function formatDateAndTime(dateTimeObject) {
170 return dateTimeObject.toLocaleString(DateTime.DATETIME_HUGE);
173// Formats a DateTime object to something like "5:00 PM"
174export function formatTime(dateTimeObject) {
175 const dt = dateTimeObject.toLocaleString(DateTime.TIME_SIMPLE);
176 return dt.replace(/:00/g, "");
179export function formatTimeZone(dateTimeObject) {
180 return dateTimeObject.offsetNameShort;
183// Formats a DateTime object to something like "Jul 15, 2021"
184export function formatDate(dateTimeObject) {
185 return dateTimeObject.toLocaleString(DateTime.DATE_FULL);
188// export function generateCrumbs(path) {
189// const crumbs = path.split('/')
190// return crumbs
191// .filter(v => v !== '')
192// .map((crumb, index) => ({
193// name: capitalize(crumb),
194// key: crumb,
195// path: crumbs.slice(0, index+2).join('/'),
196// }))
197// }
199/**
200 * buildPageTree is written to provide a recursive index of posts
201 * for nested directories for creating sidebars
202 * @param {string} path Absolute path to the subdirectory.
203 * @returns {Object.<string,string|Object<string,string[]|number>>} Title of the directory, child pages, and child directories (recursive structure).
204 *
205 */
207// type PageTree =
208// { title: string,
209// pages: [{
210// title: string,
211// base: string,
212// slug: string,
213// weight: number,
214// }],
215// children: { [string]: PageTree },
216// }
218export function buildPageTree(path, ordering = "", content = false) {
219 const metadata = matter(fs.readFileSync(join(path, "_index.md")), options);
220 // 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
221 const children = fs.readdirSync(path, { withFileTypes: true });
222 // get a list of files
223 const pages = children.filter((f) => f.isFile() && f.name !== "_index.md");
224 // get a list of folders
225 const subdirs = children.filter((f) => f.isDirectory());
226 // return a pagetree datastructure
227 return {
228 title: metadata.data.title,
229 pages: pages
230 .map((page) => {
231 // retrieve topmatter as JSON
232 const { content, data } = matter(
233 fs.readFileSync(join(path, page.name)),
234 options
235 );
237 return {
238 title: data.title,
239 base: page.name,
240 slug: page.name.replace(/.md$/, ""),
241 ...(ordering === "weight" && { weight: data?.weight ?? 0 }),
242 ...(ordering === "date" && {
243 date: data?.date ?? "2000-01-01T00:00:00.000Z",
244 }),
245 };
246 })
247 .sort((a, b) => a.weight - b.weight),
248 children: Object.fromEntries(
249 subdirs
250 .sort((a, b) => {
251 const aMetadata = matter(
252 fs.readFileSync(join(path, a.name, "_index.md")),
253 options
254 );
255 const bMetadata = matter(
256 fs.readFileSync(join(path, b.name, "_index.md")),
257 options
258 );
259 return aMetadata?.data?.weight - bMetadata?.data?.weight || 0;
260 })
261 .map((subdir) => [
262 subdir.name,
263 buildPageTree(join(path, subdir.name), ordering, content),
264 ])
265 ),
266 };
269export const getPage = (path) => {
270 try {
271 let fileContents = fs.readFileSync(`${path}.md`, "utf8");
272 if (fileContents) {
273 const { data, content } = matter(fileContents, options);
274 return { data, content };
275 }
276 } catch {
277 try {
278 let fileContents = fs.readFileSync(`${path}/_index.md`, "utf8");
279 if (fileContents) {
280 const { data, content } = matter(fileContents, options);
281 return { data, content };
282 }
283 } catch {
284 console.error("no md file for slug");
285 }
286 }
287};
289// Returns an array of unique grants categories
290export function getGrantsCategories() {
291 const paths = fs.readdirSync(directories("grants"));
292 const uniqueCategories = paths
293 // Get only markdown files
294 .filter((path) => parse(path).ext === ".md")
295 // Parse frontmatter
296 .map((path) =>
297 matter(fs.readFileSync(join(directories("grants"), path)), options)
298 )
299 // Each markdown file has an array of categories. Collapse these arrays into one array.
300 .reduce((acc, metadata) => {
301 return [...acc, ...metadata.data.taxonomies.grant_category];
302 }, [])
303 // Deduplicate single array of categories such that the output is an array where each category is represented only once.
304 .reduce((acc, uniqueItem) => {
305 if (acc.includes(uniqueItem)) {
306 return acc;
307 }
308 return [...acc, uniqueItem];
309 }, []);
310 return uniqueCategories;
313// Returns an array of unique grants types
314export function getGrantsTypes() {
315 const paths = fs.readdirSync(directories("grants"));
316 const uniqueTypes = paths
317 // Get only markdown files
318 .filter((path) => parse(path).ext === ".md")
319 // Parse frontmatter to JSON
320 .map((path) =>
321 matter(fs.readFileSync(join(directories("grants"), path)), options)
322 )
323 // Each markdown file has an array of types. Collapse these arrays into one array.
324 .reduce((acc, metadata) => {
325 return [...acc, ...metadata.data.taxonomies.grant_type];
326 }, [])
327 // Deduplicate single array of types such that the output is an array where each type is represented only once.
328 .reduce((acc, uniqueItem) => {
329 if (acc.includes(uniqueItem)) {
330 return acc;
331 }
332 return [...acc, uniqueItem];
333 }, []);
334 return uniqueTypes;
337// Returns a number which represents how many grants are open, or uncompleted
338export function getOpenGrantsCount() {
339 const paths = fs.readdirSync(directories("grants"));
340 const count = paths
341 // Get only markdown files
342 .filter((path) => parse(path).ext === ".md")
343 // Parse frontmatter to JSON
344 .map((path) =>
345 matter(fs.readFileSync(join(directories("grants"), path)), options)
346 )
347 // Count grants which are both uncompleted and not assigned
348 .reduce((acc, metadata) => {
349 if (
350 !metadata.data.extra.completed &&
351 !metadata.data.extra.canceled &&
352 metadata.data.extra.assignee === ""
353 ) {
354 return acc + 1;
355 }
356 return acc;
357 }, 0);
358 return count;