on d2fbd56 help sign in
urbit.org/lib/lib.js
289 lines 8.6 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};
16export function getPostSlugs(key) {
17 return fs.readdirSync(directories[key]);
20// NB Gavin: This could be much simpler if we converted everything to a more common standard, like YAML
21const options = {
22 engines: {
23 toml: toml.parse.bind(toml),
24 },
25 language: "toml",
26 delimiters: "+++",
27};
29export function getPostBySlug(slug, fields = [], key) {
30 const realSlug = slug.replace(/\.md$/, "");
31 const fullPath = join(directories[key], `${realSlug}.md`);
32 const fileContents = fs.readFileSync(fullPath, "utf8");
33 const { data, content } = matter(fileContents, options);
34 const items = {};
36 // Ensure only the minimal needed data is exposed
37 fields.forEach((field) => {
38 if (field === "slug") {
39 items[field] = realSlug;
40 }
42 if (field === "content") {
43 items[field] = content;
44 }
46 if (data[field]) {
47 items[field] = data[field];
48 }
49 });
51 // 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.
52 items.date = JSON.stringify(items.date).replace(/\"/g, "");
54 return items;
57export function getAllPosts(fields = [], key) {
58 const slugs = getPostSlugs(key);
59 const posts = slugs
60 .map((slug) => getPostBySlug(slug, fields, key))
61 // sort posts by date in descending order
62 .sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
64 return posts;
67function getAllPaths(dirPath, arrayOfFiles) {
68 let files = fs.readdirSync(dirPath);
70 arrayOfFiles = arrayOfFiles || [];
72 files.forEach(function (file) {
73 if (fs.statSync(dirPath + "/" + file).isDirectory()) {
74 arrayOfFiles = getAllPaths(dirPath + "/" + file, arrayOfFiles);
75 } else {
76 arrayOfFiles.push(join(dirPath, "/", file));
77 }
78 });
80 return arrayOfFiles;
83export function getDocsPaths() {
84 return getAllPaths(directories.docs, []);
87export function getNextPost(slug, fields = [], key) {
88 let resultPost = null;
90 getAllPosts(fields, key).forEach((post, index, array) => {
91 if (post.slug === slug) {
92 resultPost = array[index - 1];
93 }
94 });
95 return resultPost;
98export function getPreviousPost(slug, fields = [], key) {
99 let resultPost = null;
101 getAllPosts(fields, key).forEach((post, index, array) => {
102 if (post.slug === slug) {
103 resultPost = array[index + 1];
104 }
105 });
106 return resultPost;
109export function formatDate(d) {
110 // const wk = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
111 const mn = [
112 "January",
113 "February",
114 "March",
115 "April",
116 "May",
117 "June",
118 "July",
119 "August",
120 "September",
121 "October",
122 "November",
123 "December",
124 ];
125 return mn[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear();
128// export function capitalize(str) {
129// return str.charAt(0).toUpperCase() + str.slice(1);
130// }
132// export function generateCrumbs(path) {
133// const crumbs = path.split('/')
134// return crumbs
135// .filter(v => v !== '')
136// .map((crumb, index) => ({
137// name: capitalize(crumb),
138// key: crumb,
139// path: crumbs.slice(0, index+2).join('/'),
140// }))
141// }
143/**
144 * buildPageTree is written to provide a recursive index of posts
145 * for nested directories for creating sidebars
146 * @param {string} path Absolute path to the subdirectory.
147 * @returns {Object.<string,string|Object<string,string[]|number>>} Title of the directory, child pages, and child directories (recursive structure).
148 *
149 */
151// type PageTree =
152// { title: string,
153// pages: [{
154// title: string,
155// base: string,
156// slug: string,
157// weight: number,
158// }],
159// children: { [string]: PageTree },
160// }
162export function buildPageTree(path, ordering = "", content = false) {
163 // get metadata from _index.md file in each folder. This is where page ordering (weight) is stored.
164 const metadata = matter(fs.readFileSync(join(path, "_index.md")), options);
165 // 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
166 const children = fs.readdirSync(path, { withFileTypes: true });
167 // get a list of files
168 const pages = children.filter((f) => f.isFile() && f.name !== "_index.md");
169 // get a list of folders
170 const subdirs = children.filter((f) => f.isDirectory());
171 // return a pagetree datastructure
172 return {
173 title: metadata.data.title,
174 pages: pages
175 .map((page) => {
176 // retrieve topmatter as JSON
177 const { content, data } = matter(
178 fs.readFileSync(join(path, page.name)),
179 options
180 );
182 return {
183 title: data.title,
184 base: page.name,
185 slug: page.name.replace(/.md$/, ""),
186 ...(ordering === "weight" && { weight: data?.weight ?? 0 }),
187 ...(ordering === "date" && {
188 date: data?.date ?? "2000-01-01T00:00:00.000Z",
189 }),
190 };
191 })
192 // Sort files according to weight. Weight can be a date or number. The choice between the two is set by the 'ordering' param.
193 .sort((a, b) => a.weight - b.weight),
194 children: Object.fromEntries(
195 subdirs.map((subdir) => [
196 subdir.name,
197 buildPageTree(join(path, subdir.name), ordering, content),
198 ])
199 ),
200 };
203export const getPage = (path) => {
204 try {
205 let fileContents = fs.readFileSync(`${path}.md`, "utf8");
206 if (fileContents) {
207 const { data, content } = matter(fileContents, options);
208 return { data, content };
209 }
210 } catch {
211 try {
212 let fileContents = fs.readFileSync(`${path}/_index.md`, "utf8");
213 if (fileContents) {
214 const { data, content } = matter(fileContents, options);
215 return { data, content };
216 }
217 } catch {
218 console.error("no md file for slug");
219 }
220 }
221};
223// Returns an array of unique grants categories
224export function getGrantsCategories() {
225 const paths = fs.readdirSync(directories.grants);
226 const uniqueCategories = paths
227 // Get only markdown files
228 .filter((path) => parse(path).ext === ".md")
229 // Parse frontmatter
230 .map((path) =>
231 matter(fs.readFileSync(join(directories.grants, path)), options)
232 )
233 // Each markdown file has an array of categories. Collapse these arrays into one array.
234 .reduce((acc, metadata) => {
235 return [...acc, ...metadata.data.taxonomies.grant_category];
236 }, [])
237 // Deduplicate single array of categories such that the output is an array where each category is represented only once.
238 .reduce((acc, uniqueItem) => {
239 if (acc.includes(uniqueItem)) {
240 return acc;
241 }
242 return [...acc, uniqueItem];
243 }, []);
244 return uniqueCategories;
247// Returns an array of unique grants types
248export function getGrantsTypes() {
249 const paths = fs.readdirSync(directories.grants);
250 const uniqueTypes = paths
251 // Get only markdown files
252 .filter((path) => parse(path).ext === ".md")
253 // Parse frontmatter to JSON
254 .map((path) =>
255 matter(fs.readFileSync(join(directories.grants, path)), options)
256 )
257 // Each markdown file has an array of types. Collapse these arrays into one array.
258 .reduce((acc, metadata) => {
259 return [...acc, ...metadata.data.taxonomies.grant_type];
260 }, [])
261 // Deduplicate single array of types such that the output is an array where each type is represented only once.
262 .reduce((acc, uniqueItem) => {
263 if (acc.includes(uniqueItem)) {
264 return acc;
265 }
266 return [...acc, uniqueItem];
267 }, []);
268 return uniqueTypes;
271// Returns a number which represents how many grants are open, or uncompleted
272export function getOpenGrantsCount() {
273 const paths = fs.readdirSync(directories.grants);
274 const count = paths
275 // Get only markdown files
276 .filter((path) => parse(path).ext === ".md")
277 // Parse frontmatter to JSON
278 .map((path) =>
279 matter(fs.readFileSync(join(directories.grants, path)), options)
280 )
281 // Count non-completed grants
282 .reduce((acc, metadata) => {
283 if (metadata.data.extra.completed) {
284 return acc;
285 }
286 return acc + 1;
287 }, 0);
288 return count;