From 63ac3c83e5a4bdc4e08859d23536f750d3d79ee7 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 10 Mar 2022 20:30:55 -0600 Subject: [PATCH] Add /api/ --- .gitignore | 1 + .vitepress/config.js | 60 ++++++++++++++- .vitepress/theme/custom.css | 15 ++++ copyComponents.js | 32 ++++++++ docgen.config.js | 149 ++++++++++++++++++++++++++++++++++++ package.json | 3 +- 6 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 copyComponents.js create mode 100644 docgen.config.js diff --git a/.gitignore b/.gitignore index 1c879fd9..ee01f9a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .vitepress/dist api +components diff --git a/.vitepress/config.js b/.vitepress/config.js index 0e2ce437..4ecc1bfb 100644 --- a/.vitepress/config.js +++ b/.vitepress/config.js @@ -1,11 +1,12 @@ const fs = require("fs"); +const path = require("path"); module.exports = { lang: "en-US", title: 'Profectus', description: 'A game engine that grows with you.', head: [ - ['link', { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital@0;1' }], + ['link', { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,400;0,600;1,400' }], ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }], ['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' }], ['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' }], @@ -71,7 +72,7 @@ module.exports = { function generateAPISidebar() { const modules = fs.readdirSync("./api/modules").map(file => file.substr(0, file.length - 3)); - const folders = {}; + const moduleFolders = {}; modules.forEach(file => { // Split by _, but not break_eternity const pieces = file.replace("break_eternity", "break~eternity").split(/_/).map(piece => piece === "break~eternity" ? "break_eternity" : piece); @@ -82,9 +83,60 @@ function generateAPISidebar() { acc[curr] = { text: camelToTitle(curr), children: [] }; } return acc[curr].children; - }, folders).push(lineItem); + }, moduleFolders).push(lineItem); + }); + const sidebar = processFolders(moduleFolders); + + const componentFolders = []; + walk("./api/components", componentFolders); + sidebar.unshift({ + text: "Components", + children: componentFolders + }); + + walk("./api/features", sidebar.find(item => item.text === "Features").children); + + sort(sidebar); + + return sidebar; +} + +function sort(sidebar) { + sidebar.filter(sidebar => !!sidebar.children).forEach(item => sort(item.children)); + sidebar.sort((a, b) => { + if (a.children && !b.children) { + return -1; + } else if (!a.children && b.children) { + return 1; + } else if (a.text > b.text) { + return 1; + } else if (a.text < b.text) { + return -1; + } else { + return 0; + } + }); +} + +function walk(dir, sidebar) { + const files = fs.readdirSync(dir); + files.forEach(file => { + const resolvedFile = path.join(dir, file); + const stat = fs.statSync(resolvedFile); + if (stat.isDirectory()) { + let folder = sidebar.find(item => item.text === camelToTitle(file)); + if (!folder) { + folder = { + text: camelToTitle(file), + children: [] + }; + sidebar.push(folder); + } + walk(resolvedFile, folder.children); + } else { + sidebar.push({ text: camelToTitle(file.substr(0, file.length - 3)), link: resolvedFile.substr(0, resolvedFile.length - 3) + ".html" }); + } }); - return processFolders(folders); } function camelToTitle(camel) { diff --git a/.vitepress/theme/custom.css b/.vitepress/theme/custom.css index d444f52f..024bf0f6 100644 --- a/.vitepress/theme/custom.css +++ b/.vitepress/theme/custom.css @@ -22,3 +22,18 @@ tr:nth-child(2n) { font-size: 0.9rem; font-weight: 400; } + +p.sidebar-link-item { + color: var(--c-text-lighter); +} + +pre { + display: inline-block; + margin: 0; + border-radius: 3px; + padding: 0.25rem 0.5rem; + font-family: var(--code-font-family); + font-size: 0.85em; + color: var(--c-text-light); + background-color: var(--code-inline-bg-color); +} diff --git a/copyComponents.js b/copyComponents.js new file mode 100644 index 00000000..11dade5b --- /dev/null +++ b/copyComponents.js @@ -0,0 +1,32 @@ +const fs = require("fs"); +const path = require("path"); + +function walk(dir, dest) { + fs.readdir(dir, (err, list) => { + list.forEach(file => { + const resolvedFile = path.resolve(dir, file); + fs.stat(resolvedFile, (err, stat) => { + if (stat.isDirectory()) { + walk(resolvedFile, path.resolve(dest, file)); + } else { + const stream = fs.createReadStream(resolvedFile); + let lineCount = 0; + stream.on("data", buffer => { + let idx = -1; + lineCount--; // Because the loop will run once for idx=-1 + do { + idx = buffer.indexOf(10, idx + 1); + lineCount++; + } while (idx !== -1); + if (lineCount > 4) { + stream.destroy(); + fs.mkdirSync(dest, { recursive: true }); + fs.copyFileSync(resolvedFile, path.resolve(dest, file)); + } + }); + } + }); + }); + }); +} +walk("./components", "./api"); diff --git a/docgen.config.js b/docgen.config.js new file mode 100644 index 00000000..ff4f9a22 --- /dev/null +++ b/docgen.config.js @@ -0,0 +1,149 @@ +const path = require('path') +const mdclean = require('vue-docgen-cli/lib/templates/utils').mdclean + +module.exports = { + componentsRoot: 'profectus/src', // the folder where CLI will start searching for components. + components: '**/[A-Z]*.vue', // the glob to define what files should be documented as components (relative to componentRoot) + outDir: 'components', // folder to save components docs in (relative to the current working directry) + apiOptions: { + jsx: true // tell vue-docgen-api that your components are using JSX to avoid conflicts with TypeScript syntax + }, + getDestFile: (file, config) => + path.join(config.outDir, file).replace(/\.vue$/, 'Component.md'), // specify the name of the output md file + templates: { + // global component template wrapping all others see #templates + component, + events: (events, opt) => displayUsedCols("Events", eventCols, eventShouldDisplayCol, eventGetDisplay, events, opt), + methods: require('vue-docgen-cli/lib/templates/methods').default, + props: (props, opt) => displayUsedCols("Props", propCols, propShouldDisplayCol, propGetDisplay, props, opt), + slots: (slots, opt) => displayUsedCols("Slots", slotCols, slotShouldDisplayCol, slotGetDisplay, slots, opt), + // static template to display as a tag if component is functional + functionalTag: '**functional**' + } +} + +function component(renderedUsage, // props, events, methods and slots documentation rendered + doc, // the object returned by vue-docgen-api + config, // the local config, useful to know the context + fileName, // the name of the current file in the doc (to explain how to import it) + requiresMd, // a list of all the documentation files + // attached to the component documented. It includes documentation of subcomponents + { isSubComponent, hasSubComponents }) { + const { displayName, description, docsBlocks } = doc + return ` + # Component: ${displayName} + + ${description ? '> ' + description : ''} + + ${renderedUsage.props} + ${renderedUsage.methods} + ${renderedUsage.events} + ${renderedUsage.slots} + ${docsBlocks?.length ? '---\n' + docsBlocks.join('\n---\n') : ''} + ` +} + +const eventCols = ["Name", "Description", "Properties"]; +const eventShouldDisplayCol = { + [eventCols[0]]: event => event.name, + [eventCols[1]]: event => event.description, + [eventCols[2]]: event => event.properties?.length +} +const eventGetDisplay = { + [eventCols[0]]: event => `
${clean(event.name)}
`, + [eventCols[1]]: event => clean(event.description || ""), + [eventCols[2]]: event => { + if (!event.properties) return ""; + return event.properties.map(property => { + const { name, description, type } = property + if (!type) { + return '' + } + return `**${name}**
${clean(type.names.length ? type.names.join(', ') : '')}
- ${description}` + }).join('
') + } +} + +const propCols = ["Name", "Types", "Description", "Values", "Default"]; +const propShouldDisplayCol = { + [propCols[0]]: pr => pr.name, + [propCols[1]]: pr => pr.type?.name, + [propCols[2]]: pr => pr.description || pr.tags, + [propCols[3]]: pr => pr.values, + [propCols[4]]: pr => pr.defaultValue +} +const propGetDisplay = { + [propCols[0]]: pr => `
${clean(pr.name)}
` + (pr.required ? "*" : ""), + [propCols[1]]: pr => { + let n = pr.type?.name ?? '' + if (n === "union") n = pr.type?.elements.map(el => el.name).join(' | ') + if (n === "Array") n = (pr.type?.elements.length > 1 ? "(" : "") + pr.type?.elements.map(el => el.name).join(' | ') + (pr.type?.elements.length > 1 ? ")" : "") + "[]" + return n ? '
' + clean(n) + '
' : '' + }, + [propCols[2]]: pr => clean((pr.description ?? '') + renderTags(pr.tags)), + [propCols[3]]: pr => clean(pr.values?.map(pv => `\`${pv}\``).join(', ') ?? '-'), + [propCols[4]]: pr => clean(pr.defaultValue?.value ?? '') +} + +const slotCols = ["Name", "Description", "Bindings"]; +const slotShouldDisplayCol = { + [slotCols[0]]: slot => slot.name, + [slotCols[1]]: slot => slot.description, + [slotCols[2]]: slot => slot.bindings?.length +} +const slotGetDisplay = { + [slotCols[0]]: slot => `
${clean(slot.name)}
`, + [slotCols[1]]: slot => clean(slot.description || ""), + [slotCols[2]]: slot => { + if (!slot.bindings) return ""; + return slot.bindings.map(binding => { + const { name, description, type } = binding + if (!type) { + return '' + } + return `**${name}**
${
+        type.name === 'union' && type.elements
+          ? type.elements.map(({ name: insideName }) => insideName).join(', ')
+          : type.name
+      }
- ${description}` + }).join('
') + } +} + +function displayUsedCols(title, cols, shouldDisplayCol, getDisplay, rows, opt) { + cols = cols.filter(col => rows.some(shouldDisplayCol[col])); + if (!cols) return ""; + + let output = `\n${opt.isSubComponent || opt.hasSubComponents ? '#' : ''}## ${title}\n|`; + cols.forEach(col => (output += ` ${col} |`)); + output += `\n|`; + cols.forEach(col => (output += ' :-- |')); + output += '\n'; + + rows.forEach(row => { + output += "|"; + cols.forEach(col => (output += ` ${getDisplay[col](row)} |`)); + output += "\n"; + }); + + return output; +} + +function isTag(v) { + return !!(v).content +} + +function renderTags(tags) { + if (!tags) { + return '' + } + return Object.entries(tags) + .map(([tag, values]) => { + return values.map(v => `
\`@${tag}\` ${isTag(v) ? v.content : v.description}`).join('') + }) + .join('') +} + +function clean(str) { + return mdclean(str).replace(//g, '_br/_').replace(/>/g, '\\>').replace(/_br\/_/g, '
') +} diff --git a/package.json b/package.json index 19eb5fdf..44619e25 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "serve": "vitepress serve", "dev": "vitepress dev", "build": "vitepress build", - "typedoc": "cd profectus && vue-typedoc --logLevel Verbose --plugin typedoc-plugin-markdown --hideBreadcrumbs --hideInPageTOC --entryDocument index.md --plugin typedoc-plugin-rename-defaults --plugin typedoc-plugin-mdn-links" + "typedoc": "cd profectus && vue-typedoc --logLevel Verbose --plugin typedoc-plugin-markdown --hideBreadcrumbs --hideInPageTOC --entryDocument index.md --plugin typedoc-plugin-rename-defaults --plugin typedoc-plugin-mdn-links", + "docgen": "vue-docgen -c docgen.config.js && node copyComponents.js" }, "repository": "git+https://github.com/profectus-engine/profectus-docs.git", "author": "thepaperpilot",