3593799d9e
Adds reference docs, guide in the "Editor" article, and examples for the side effects manager. There are 4 new examples: 1. Before create/update shape - constrains shapes to be places within a circle 2. Before delete shape - prevent red shapes from being deleted 3. After create/update shape - make sure there's only ever one red shape on the page at a time 4. After delete shape - delete frames after their last child is deleted As these examples all require fairly specific configurations of shapes (or are hard to understand without some visual hinting in the case of placing shapes within a circle), I've included a `createDemoShapes` function in each of these which makes sure the examples start with shapes that will quickly show you the side effects in action. I've kept these separate from the main code (in a function at the bottom), so hopefully that won't be a source of confusion to anyone working from these examples. ### Change Type - [x] `docs` — Changes to the documentation, examples, or templates. - [x] `improvement` — Improving existing features
228 lines
6.4 KiB
TypeScript
228 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}`,
|
|
}
|
|
|
|
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
|
|
}
|