tldraw/apps/docs/scripts/functions/generateSection.ts
alex fba82ed924
Generated docs cleanup (#3935)
Our generated docs are pretty verbose and space inefficient. This diff
has a few design tweaks to try and make sure that the information that's
emphasised is the stuff that's most important, and makes the typical
docs item use a bit less space in the process.


![image](https://github.com/tldraw/tldraw/assets/1489520/df433ae0-1400-4f5b-951e-e25869621a40)



### Change Type

- [x] `docs` — Changes to the documentation, examples, or templates.
- [x] `improvement` — Improving existing features
2024-06-13 16:04:12 +00:00

229 lines
6.4 KiB
TypeScript

import fs from 'fs'
import matter from 'gray-matter'
import path from 'path'
import {
Article,
ArticleStatus,
Articles,
Category,
InputSection,
Section,
} from '../../types/content-types'
import { CONTENT_DIR } from '../utils'
export function generateSection(section: InputSection, articles: Articles, index: number): Section {
// 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 isExamples = section.id === 'examples'
const dir = isExamples
? path.join(process.cwd(), '..', 'examples', 'src', 'examples')
: path.join(CONTENT_DIR, section.id)
const files = fs.readdirSync(dir, { withFileTypes: false })
const isGenerated = section.id === 'reference'
for (const file of files) {
const filename = file.toString()
if (filename.startsWith('.')) continue
const pathname = isExamples ? path.join(dir, filename, 'README.md') : path.join(dir, filename)
const fileContent = fs.readFileSync(pathname).toString()
const extension = path.extname(filename)
const articleId = filename.replace(extension, '')
const parsed = matter({ content: fileContent }, {})
// If we're in prod and the article isn't published, skip it
if (
process.env.NODE_ENV !== 'development' &&
!isExamples &&
parsed.data.status !== 'published'
) {
continue
}
// 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)
// By default, the category is ucg (uncategorized, with the section id in the id)
const { category: categoryId = section.id + '_ucg', author = 'api' } = parsed.data
const isUncategorized = categoryId === section.id + '_ucg'
const isIndex = articleId === section.id
const componentCode = parsed.data.component
? fs
.readFileSync(
path.join(
dir,
filename,
`${parsed.data.component}${parsed.data.component.endsWith('.tsx') ? '' : '.tsx'}`
)
)
.toString()
: null
const componentCodeFiles: { [key: string]: string } = {}
if (parsed.data.component) {
const exampleParentDirectory = path.join(dir, filename)
const codeFilenames = fs.readdirSync(exampleParentDirectory, {
withFileTypes: true,
recursive: true,
})
codeFilenames
.filter(
(file) =>
!file.isDirectory() &&
file.name !== 'README.md' &&
file.name.replace('.tsx', '') !==
parsed.data.component.replace('./', '').replace('.tsx', '')
)
.forEach((file) => {
componentCodeFiles[file.name] = fs
.readFileSync(path.join(file.path, file.name))
.toString()
})
}
const article: Article = {
id: articleId,
type: 'article',
sectionIndex: 0,
groupIndex: -1,
groupId: parsed.data.group ?? null,
categoryIndex: parsed.data.order ?? parsed.data.priority ?? -1,
sectionId: section.id,
author,
categoryId,
status: parsed.data.status ?? ArticleStatus.Draft,
title: parsed.data.title ?? 'Untitled article',
description: parsed.data.description,
hero: parsed.data.hero ?? null,
date: parsed.data.date ? new Date(parsed.data.date).toISOString() : null,
keywords: parsed.data.keywords ?? [],
sourceUrl: isGenerated // if it's a generated API doc, then we don't have a link
? parsed.data.sourceUrl ?? null
: `${section.id}/${articleId}${extension}`,
componentCode,
componentCodeFiles: componentCode ? JSON.stringify(componentCodeFiles) : null,
content: parsed.content,
path:
section.id === 'getting-started'
? `/${articleId}`
: isUncategorized
? `/${section.id}/${articleId}`
: `/${section.id}/${categoryId}/${articleId}`,
apiTags: parsed.data.apiTags ?? null,
}
if (isExamples) {
// split content above the first ---
const splitUp = parsed.content.split('---\n')
article.description = splitUp[0]
article.content = splitUp.slice(1).join('---\n')
}
if (isIndex) {
article.categoryIndex = -1
article.sectionIndex = -1
articles[section.id + '_index'] = article
} else {
if (category) {
if (article.id === category.id) {
article.categoryIndex = -1
article.sectionIndex = -1
articles[category.id + '_index'] = article
} else {
_categoryArticles[categoryId].push(article)
}
} else {
_ucg.push(article)
}
}
}
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
articles[article.id] = article
sectionIndex++
})
const categories: Category[] = [
{
id: section.id + '_ucg',
type: 'category',
sectionId: section.id,
index: 0,
title: 'Uncategorized',
description: 'Articles that do not belong to a category.',
groups: [],
path: `/${section.id}/ucg`,
content: null,
hero: null,
},
]
// Sort categorized articles by date and add them to the articles table
section.categories.forEach((inputCategory, i) => {
const categoryArticles = _categoryArticles[inputCategory.id]
categoryArticles.sort(sortArticles).forEach((article, i) => {
article.categoryIndex = i
article.sectionIndex = sectionIndex
articles[article.id] = article
sectionIndex++
})
if (categoryArticles.length) {
categories.push({
...inputCategory,
type: 'category',
sectionId: section.id,
index: i + 1,
path: `/${section.id}/${inputCategory.id}`,
content: null,
hero: null,
groups: inputCategory.groups.map(({ id }, i) => ({
id,
title: id,
index: i,
type: 'group',
sectionId: section.id,
categoryId: inputCategory.id,
description: null,
content: null,
path: `/${section.id}/${inputCategory.id}/${id}`,
})),
})
}
})
return {
...section,
type: 'section',
sidebar_behavior: section.sidebar_behavior,
index,
categories,
content: '',
hero: section.hero ?? null,
path: `/${section.id}`,
}
}
const sortArticles = (articleA: Article, articleB: Article) => {
const { categoryIndex: categoryIndexA, title: titleA } = articleA
const { categoryIndex: categoryIndexB, title: titleB } = articleB
return categoryIndexA === categoryIndexB
? titleA.localeCompare(titleB)
: categoryIndexA - categoryIndexB
}