tldraw/apps/docs/scripts/generateContent.ts

230 lines
6.4 KiB
TypeScript
Raw Normal View History

2023-04-25 11:01:25 +00:00
import fs from 'fs'
import matter from 'gray-matter'
import path from 'path'
import authors from '../content/authors.json'
import sections from '../content/sections.json'
import {
Article,
Articles,
Category,
GeneratedContent,
Group,
MarkdownContent,
Section,
Status,
} from '../types/content-types'
const { log: nicelog } = console
2023-04-25 11:01:25 +00:00
type InputCategory = {
id: string
title: string
description: string
groups: Group[]
}
type InputSection = {
id: string
title: string
description: string
categories: InputCategory[]
}
function generateSection(
section: InputSection,
content: MarkdownContent,
articles: Articles
): Section {
// A temporary table of categories
const _categories: Record<string, Category> = {}
// Uncategorized articles
const _ucg: Article[] = []
// A temporary table of articles mapped to categories
const _categoryArticles: Record<string, Article[]> = Object.fromEntries(
section.categories.map((category) => [category.id, []])
)
// The file directory for this section
const dir = path.join(process.cwd(), 'content', section.id)
fs.readdirSync(dir, { withFileTypes: false }).forEach((result: string | Buffer) => {
try {
const filename = result.toString()
const fileContent = fs.readFileSync(path.join(dir, filename)).toString()
const extension = path.extname(filename)
const articleId = filename.replace(extension, '')
const parsed = matter({ content: fileContent }, {})
if (process.env.NODE_ENV !== 'development' && parsed.data.status !== 'published') {
return
}
// If a category was provided but that category was not found in the section, throw an error
const category =
parsed.data.category && section.categories.find((c) => c.id === parsed.data.category)
if (parsed.data.category && !category) {
throw Error(
`Could not find a category for section ${section.id} with id ${parsed.data.category}.`
)
}
if (parsed.data.author && !authors[parsed.data.author as keyof typeof authors]) {
throw Error(`Could not find an author with id ${parsed.data.author}.`)
}
// By default, the category is ucg (uncategorized)
const { category: categoryId = 'ucg' } = parsed.data
const article: Article = {
id: articleId,
sectionIndex: 0,
groupIndex: -1,
groupId: parsed.data.group ?? null,
categoryIndex: parsed.data.order ?? -1,
sectionId: section.id,
categoryId: parsed.data.category ?? 'ucg',
status: parsed.data.status ?? Status.Draft,
title: parsed.data.title ?? 'Article',
description: parsed.data.description ?? 'An article for the docs site.',
hero: parsed.data.hero ?? null,
date: parsed.data.date ? new Date(parsed.data.date).toISOString() : null,
keywords: parsed.data.keywords ?? [],
next: null,
prev: null,
author: parsed.data.author
? authors[parsed.data.author as keyof typeof authors] ?? null
: null,
sourceUrl: `https://github.com/tldraw/tldraw/tree/main/apps/docs/content/${section.id}/${articleId}${extension}`,
2023-04-25 11:01:25 +00:00
}
if (article.id === section.id) {
article.categoryIndex = -1
article.sectionIndex = -1
articles[section.id + '_index'] = article
content[section.id + '_index'] = parsed.content
} else {
if (category) {
if (article.id === category.id) {
article.categoryIndex = -1
article.sectionIndex = -1
articles[category.id + '_index'] = article
content[category.id + '_index'] = parsed.content
} else {
_categoryArticles[categoryId].push(article)
content[articleId] = parsed.content
}
} else {
_ucg.push(article)
content[articleId] = parsed.content
}
}
} catch (e) {
console.error(e)
}
})
const sortArticles = (articleA: Article, articleB: Article) => {
const { categoryIndex: categoryIndexA, date: dateA = '01/01/1970' } = articleA
const { categoryIndex: categoryIndexB, date: dateB = '01/01/1970' } = articleB
return categoryIndexA === categoryIndexB
? new Date(dateB!).getTime() > new Date(dateA!).getTime()
? 1
: -1
: categoryIndexA < categoryIndexB
? -1
: 1
}
let sectionIndex = 0
// Sort ucg articles by date and add them to the articles table
_ucg.sort(sortArticles).forEach((article, i) => {
article.categoryIndex = i
article.sectionIndex = sectionIndex++
article.prev = _ucg[i - 1]?.id ?? null
article.next = _ucg[i + 1]?.id ?? null
articles[article.id] = article
})
// Sort categorized articles by date and add them to the articles table
section.categories.forEach((category) => {
const categoryArticles = _categoryArticles[category.id]
categoryArticles.sort(sortArticles).forEach((article, i) => {
article.categoryIndex = i
article.sectionIndex = sectionIndex++
article.prev = categoryArticles[i - 1]?.id ?? null
article.next = categoryArticles[i + 1]?.id ?? null
articles[article.id] = article
})
_categories[category.id] = {
...category,
articleIds: categoryArticles.map((article) => article.id),
}
})
return {
...section,
categories: [
{
id: 'ucg',
title: 'Uncategorized',
description: 'Articles that do not belong to a category.',
groups: [],
articleIds: _ucg
.sort((a, b) => a.sectionIndex - b.sectionIndex)
.map((article) => article.id),
},
...section.categories.map(({ id }) => _categories[id]).filter((c) => c.articleIds.length > 0),
],
}
}
export async function generateContent(): Promise<GeneratedContent> {
const content: MarkdownContent = {}
const articles: Articles = {}
nicelog('• Generating site content (content.json)')
2023-04-25 11:01:25 +00:00
try {
Auto content refresh for docs site (#1606) This PR improves the author experience when working on the docs site. When running `docs-dev`, the site's content will now update whenever a content file is changed. ### Context In the docs project, we generate content from two sources: from API documentation generated by api-extractor and from markdown files in the docs/content folder. Generating API docs is a relatively slow process because it involves building and parsing TypeScript declaration files for each package in the monorepo; however, generating docs from the markdown files is basically instantaneous. The same script used to address both tasks, which meant it was too slow to run on each save. Instead, the script needed to be run manually or the dev server would need to be restarted. We now split the generation into two separate scripts. First, the script runs to generate the API content; and then a second script runs to generate the markdown content. The second script also imports and combines the two sources of content. When we build the docs, both scripts are run. When a markdown file changes, the new watcher only runs the second script. This allows the site's content to be updated quickly without having to generate the API docs each time. Note that this does not incorporate live changes to package APIs, though I can't think of a time where we be developing the docs and the APIs at the same time. ### Change Type - [x] `documentation` — Changes to the documentation only
2023-06-17 09:46:46 +00:00
const outputSections: Section[] = [...(sections as InputSection[])]
2023-04-25 11:01:25 +00:00
.map((section) => generateSection(section, content, articles))
.filter((section) => section.categories.some((c) => c.articleIds.length > 0))
nicelog('✔ Generated site content.')
2023-04-25 11:01:25 +00:00
// Write to disk
Auto content refresh for docs site (#1606) This PR improves the author experience when working on the docs site. When running `docs-dev`, the site's content will now update whenever a content file is changed. ### Context In the docs project, we generate content from two sources: from API documentation generated by api-extractor and from markdown files in the docs/content folder. Generating API docs is a relatively slow process because it involves building and parsing TypeScript declaration files for each package in the monorepo; however, generating docs from the markdown files is basically instantaneous. The same script used to address both tasks, which meant it was too slow to run on each save. Instead, the script needed to be run manually or the dev server would need to be restarted. We now split the generation into two separate scripts. First, the script runs to generate the API content; and then a second script runs to generate the markdown content. The second script also imports and combines the two sources of content. When we build the docs, both scripts are run. When a markdown file changes, the new watcher only runs the second script. This allows the site's content to be updated quickly without having to generate the API docs each time. Note that this does not incorporate live changes to package APIs, though I can't think of a time where we be developing the docs and the APIs at the same time. ### Change Type - [x] `documentation` — Changes to the documentation only
2023-06-17 09:46:46 +00:00
const generatedApiContent = (await import(
path.join(process.cwd(), 'api-content.json')
)) as GeneratedContent
const contentComplete: GeneratedContent = {
sections: generatedApiContent
? [...outputSections, ...generatedApiContent.sections]
: outputSections,
content: generatedApiContent ? { ...content, ...generatedApiContent.content } : content,
articles: generatedApiContent ? { ...articles, ...generatedApiContent.articles } : articles,
}
2023-04-25 11:01:25 +00:00
fs.writeFileSync(
path.join(process.cwd(), 'content.json'),
JSON.stringify(contentComplete, null, 2)
)
return contentComplete
} catch (error) {
nicelog(`x Could not generate site content.`)
2023-04-25 11:01:25 +00:00
throw error
}
}