215 lines
5.5 KiB
TypeScript
215 lines
5.5 KiB
TypeScript
|
import {promises as fsp} from 'fs';
|
||
|
import {basename, join} from 'path';
|
||
|
import fecha from 'fecha';
|
||
|
// @ts-ignore
|
||
|
import htmlclean from 'htmlclean';
|
||
|
import marked from 'marked';
|
||
|
import nunjucks from 'nunjucks';
|
||
|
import wordWrap from 'wordwrap';
|
||
|
import {generateFeeds} from './feeds';
|
||
|
import {OfficialTopic, getTopicsFromMonth} from './official-topics';
|
||
|
import {Statistics} from './statistics';
|
||
|
|
||
|
// Regular expressions taken and slightly adapted from Tildes' Markdown code:
|
||
|
// https://gitlab.com/tildes/tildes/-/blob/89c7c13be2a7fc11d429ee1e340bf2079862eb98/tildes/tildes/lib/markdown.py#L285-296
|
||
|
const groupRegex = /\s(?<!\w)~([\w.]+)\b(?!~)/gm;
|
||
|
const mentionRegex = /\s(?<![\w\\])(?:\/?u\/|@)([\w-]+)\b/gm;
|
||
|
|
||
|
export const months: string[] = [
|
||
|
'zeroary',
|
||
|
'january',
|
||
|
'february',
|
||
|
'march',
|
||
|
'april',
|
||
|
'may',
|
||
|
'june',
|
||
|
'july',
|
||
|
'august',
|
||
|
'september',
|
||
|
'october',
|
||
|
'november',
|
||
|
'december'
|
||
|
];
|
||
|
|
||
|
export interface Post {
|
||
|
markdown: string;
|
||
|
month: number;
|
||
|
rawMarkdown: string;
|
||
|
url: string;
|
||
|
}
|
||
|
|
||
|
export interface PostList {
|
||
|
year: number;
|
||
|
posts: Post[];
|
||
|
}
|
||
|
|
||
|
async function entry(): Promise<void> {
|
||
|
nunjucks.configure(join(__dirname, '../pages/templates/'), {
|
||
|
lstripBlocks: true,
|
||
|
throwOnUndefined: true,
|
||
|
trimBlocks: true
|
||
|
});
|
||
|
|
||
|
const dataDirectory: string = join(__dirname, '../pages/data/');
|
||
|
const statistics: Statistics[] = JSON.parse(
|
||
|
await fsp.readFile(join(dataDirectory, 'statistics.json'), 'utf8')
|
||
|
);
|
||
|
const officialTopics: OfficialTopic[] = JSON.parse(
|
||
|
await fsp.readFile(join(dataDirectory, 'official-topics.json'), 'utf8')
|
||
|
);
|
||
|
|
||
|
const allPosts: PostList[] = [];
|
||
|
// Get a list of all the years we have Markdown files for.
|
||
|
const yearDirectories: string[] = await fsp.readdir(
|
||
|
join(__dirname, '../pages/posts/')
|
||
|
);
|
||
|
for (const year of yearDirectories) {
|
||
|
const postList: PostList = {
|
||
|
year: Number(year),
|
||
|
posts: []
|
||
|
};
|
||
|
// Create the output directory if it doesn't already exist.
|
||
|
await fsp.mkdir(join(__dirname, `../../public/${year}/`), {
|
||
|
recursive: true
|
||
|
});
|
||
|
// Get a list of all the Markdown files.
|
||
|
const markdownFiles: string[] = await fsp.readdir(
|
||
|
join(__dirname, `../pages/posts/${year}/`)
|
||
|
);
|
||
|
for (const file of markdownFiles) {
|
||
|
// Get the filename without the extension.
|
||
|
const fileBasename: string = basename(file, '.md');
|
||
|
|
||
|
// Find the month index in our months array.
|
||
|
const monthNumber: number = months.indexOf(fileBasename);
|
||
|
|
||
|
// Read the actual Markdown.
|
||
|
const markdown: string = await fsp.readFile(
|
||
|
join(__dirname, `../pages/posts/${year}/${file}`),
|
||
|
'utf8'
|
||
|
);
|
||
|
|
||
|
// Add the post to the list.
|
||
|
postList.posts.push({
|
||
|
markdown,
|
||
|
month: monthNumber,
|
||
|
rawMarkdown: markdown,
|
||
|
url: `${year}/${fileBasename}.html`
|
||
|
});
|
||
|
|
||
|
// Get the statistics for that month.
|
||
|
const statistic: Statistics = statistics.find(
|
||
|
val => val.year === Number(year) && val.month === monthNumber
|
||
|
)!;
|
||
|
|
||
|
const topics: OfficialTopic[] = await getTopicsFromMonth(
|
||
|
officialTopics,
|
||
|
Number(year),
|
||
|
monthNumber
|
||
|
);
|
||
|
|
||
|
let renderedHTML: string = marked(markdown);
|
||
|
|
||
|
// Replace ~group with a link to that group.
|
||
|
renderedHTML = renderedHTML.replace(
|
||
|
groupRegex,
|
||
|
' <a class="link-group" href="https://tildes.net/~$1">~$1</a>'
|
||
|
);
|
||
|
|
||
|
// Replace @person with a link to that person.
|
||
|
renderedHTML = renderedHTML.replace(
|
||
|
mentionRegex,
|
||
|
' <a class="link-user" href="https://tildes.net/user/$1">@$1</a>'
|
||
|
);
|
||
|
|
||
|
// Render the post template.
|
||
|
await renderTemplate(
|
||
|
'post.html',
|
||
|
join(__dirname, `../../public/${year}/${fileBasename}.html`),
|
||
|
{
|
||
|
fecha,
|
||
|
months,
|
||
|
officialTopics: topics,
|
||
|
post: {
|
||
|
month: monthNumber,
|
||
|
renderedHTML,
|
||
|
year: Number(year)
|
||
|
},
|
||
|
pluralize,
|
||
|
statistic
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Sort the posts to be ascending by month.
|
||
|
postList.posts.sort((a, b) => a.month - b.month);
|
||
|
allPosts.push(postList);
|
||
|
}
|
||
|
|
||
|
// Sort the post list to be descending by year.
|
||
|
allPosts.sort((a, b) => b.year - a.year);
|
||
|
await renderTemplate(
|
||
|
'posts.html',
|
||
|
join(__dirname, '../../public/posts.html'),
|
||
|
{
|
||
|
months,
|
||
|
posts: allPosts
|
||
|
}
|
||
|
);
|
||
|
|
||
|
// Sort the posts of the most recent year descending by month to select the most recent post.
|
||
|
allPosts[0].posts.sort((a, b) => b.month - a.month);
|
||
|
await renderTemplate(
|
||
|
'index.html',
|
||
|
join(__dirname, '../../public/index.html'),
|
||
|
{
|
||
|
months,
|
||
|
newestPost: {
|
||
|
year: allPosts[0].year,
|
||
|
...allPosts[0].posts[0]
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
|
||
|
await renderTemplate(
|
||
|
'attributions.html',
|
||
|
join(__dirname, '../../public/attributions.html')
|
||
|
);
|
||
|
|
||
|
await writeEmail(allPosts[0].posts[0].rawMarkdown);
|
||
|
await generateFeeds(allPosts);
|
||
|
}
|
||
|
|
||
|
export async function renderTemplate(
|
||
|
template: string,
|
||
|
location: string,
|
||
|
context: object = {}
|
||
|
): Promise<void> {
|
||
|
const html: string = htmlclean(nunjucks.render(template, context));
|
||
|
await fsp.writeFile(location, html);
|
||
|
}
|
||
|
|
||
|
export async function writeEmail(input: string): Promise<void> {
|
||
|
await fsp.mkdir(join(__dirname, '../../temp/'), {recursive: true});
|
||
|
await fsp.writeFile(
|
||
|
join(__dirname, '../../temp/email.md'),
|
||
|
wordWrap(72)('# Tildes Issue Log\n\n' + input)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
export function pluralize(
|
||
|
singular: string,
|
||
|
plural: string,
|
||
|
count: number
|
||
|
): string {
|
||
|
if (count === 1) {
|
||
|
return singular;
|
||
|
}
|
||
|
|
||
|
return plural;
|
||
|
}
|
||
|
|
||
|
if (require.main === module) {
|
||
|
entry();
|
||
|
}
|