mirror of
https://github.com/hugo-fixit/FixIt.git
synced 2026-08-24 07:18:57 +00:00
feat(gen-docs): add Hugo partials docs generation (#809)
* feat(gen-docs): add documentation generation package with SassDoc and TypeDoc support Add new `@hugo-fixit/gen-docs` package that parses Hugo config (hugo.toml) and generates documentation. Includes SassDoc annotations for SCSS variables and mixins, TypeDoc config for TypeScript source docs, and a `.sassdocrc` configuration file. * docs(readme): add browsers support section * feat(gen-docs): add Hugo partials docs generation and fix config empty sections - Add partial-parser.ts: parses Hugo partial comment blocks (@param/@return/@example) - Add partials.ts renderer: generates grouped Markdown with param tables - Add `partials` subcommand with -t/-o options for template injection - Fix config-parser: empty TOML sections (library.js/library.css) now retain descriptions - Fix config-parser: correctly distinguish section-level vs key-level comments - Update README with partials subcommand documentation
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"display": {
|
||||
"access": ["public"],
|
||||
"alias": false
|
||||
},
|
||||
"sort": ["group"]
|
||||
}
|
||||
@@ -109,7 +109,7 @@ Click the following links to generate a new repository with template:
|
||||
- Globally consistent **design language**
|
||||
- **Pagination** supported
|
||||
- Easy-to-use and self-expanding **table of contents**
|
||||
- **Multilanguage** supported and i18n ready
|
||||
- **Multilanguage** supported and I18n ready
|
||||
- Beautiful **CSS animation**
|
||||
|
||||
### Social and Comment Systems
|
||||
@@ -168,7 +168,7 @@ Click the following links to generate a new repository with template:
|
||||
|
||||
</details>
|
||||
|
||||
## Multilingual and i18n
|
||||
## Multilingual and I18n
|
||||
|
||||
FixIt supports multilingual and i18n. For more information, see the [Content Management](https://fixit.lruihao.cn/docs/content-management/introduction#multilingual) documentation. You are welcome to [contribute with a new language](https://github.com/hugo-fixit/FixIt/pulls).
|
||||
|
||||
@@ -194,6 +194,15 @@ FixIt supports multilingual and i18n. For more information, see the [Content Man
|
||||
|
||||
</details>
|
||||
|
||||
## Browsers Support
|
||||
|
||||
The FixIt theme supports the last two versions of all major browsers.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Theme Components
|
||||
|
||||
To balance **simplicity** and **extensibility**, FixIt theme provides extra [Hugo theme components](https://fixit.lruihao.cn/ecosystem/#-components) for customization.
|
||||
|
||||
@@ -194,6 +194,15 @@ FixIt 主题多语言基本配置及自动翻译等详见 [内容管理](https:/
|
||||
|
||||
</details>
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
FixIt 主题支持所有主流浏览器的最近两个版本。
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 主题组件
|
||||
|
||||
为了平衡 FixIt 主题的 **简洁性** 和 **可扩展性**,我们额外开发了一系列 [Hugo 主题组件](https://fixit.lruihao.cn/zh-cn/ecosystem/#-组件) 供用户选择。
|
||||
|
||||
@@ -15,7 +15,8 @@ export type FixItDocumentEventMap = {
|
||||
[K in keyof FixItEventMap]: CustomEvent<FixItEventMap[K]>
|
||||
}
|
||||
|
||||
type Handler<T> = T extends void
|
||||
/** Event handler type — infers the correct signature from the event payload. */
|
||||
export type Handler<T> = T extends void
|
||||
? (() => void) | ((event: CustomEvent<void>) => void)
|
||||
: (event: CustomEvent<T>) => void
|
||||
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
@forward "hugo:vars";
|
||||
@forward "hugo:vars/internal";
|
||||
|
||||
/// CSS variable prefix for all custom properties.
|
||||
/// @type String
|
||||
$prefix: fi- !default;
|
||||
|
||||
/// Root prefix for CSS custom properties (derived from $prefix).
|
||||
/// @type String
|
||||
$rootPrefix: --#{$prefix} !default;
|
||||
|
||||
/// Default header height.
|
||||
/// @type Length
|
||||
$header-height: 3.5rem !default;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Color map of admonitions
|
||||
/// Color map of admonitions.
|
||||
/// @type Map
|
||||
$admonition-color-map: (
|
||||
note: (
|
||||
color: #448aff,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Color map of basic alerts
|
||||
/// Color map of GitHub-style alerts (light theme).
|
||||
/// @type Map
|
||||
$alert-color-map: (
|
||||
note: (
|
||||
border-color: #0969da,
|
||||
@@ -22,6 +23,8 @@ $alert-color-map: (
|
||||
),
|
||||
) !default;
|
||||
|
||||
/// Color map of GitHub-style alerts (dark theme).
|
||||
/// @type Map
|
||||
$alert-color-map-dark: (
|
||||
note: (
|
||||
border-color: #316dca,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// Map of code identifiers/languages to Chroma supported languages.
|
||||
/// This file is auto-generated by `pnpm gen:lexers`, do not edit manually.
|
||||
/// @see https://github.com/hugo-fixit/FixIt/tree/main/packages/chroma-lexers
|
||||
/// See: https://github.com/hugo-fixit/FixIt/tree/main/packages/chroma-lexers
|
||||
$chroma-lexers: (
|
||||
"🔥": "Mojo",
|
||||
"1s": "OnesEnterprise",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Theme colors map
|
||||
/// Theme semantic color tokens.
|
||||
/// @type Map
|
||||
$theme-colors: (
|
||||
'primary': #1677ff,
|
||||
'secondary': #8b949e,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
@use "core/functions" as *;
|
||||
|
||||
// TODO Superseded by text-wrap: wrap; in modern browsers
|
||||
/// Apply word-wrap and overflow-wrap for cross-browser compatibility.
|
||||
/// TODO Superseded by `text-wrap: wrap;` in modern browsers.
|
||||
/// @param {String} $value - Wrap value (e.g. `break-word`)
|
||||
@mixin overflow-wrap($value) {
|
||||
word-wrap: $value;
|
||||
overflow-wrap: $value;
|
||||
@@ -43,6 +45,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a focus-visible ring outline using a pseudo-element.
|
||||
/// @param {Length} $size [$size: 1.3em] - Size of the focus ring
|
||||
/// @param {Length} $offset [$offset: 0.2rem] - Offset from the element
|
||||
@mixin focus-visible-ring($size: 1.3em, $offset: 0.2rem) {
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@use "border-radius" as *;
|
||||
@use "compatibility" as *;
|
||||
|
||||
/// Shared inline code styles using theme CSS variables.
|
||||
@mixin inline-code() {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use "core/functions" as *;
|
||||
@use "z-index" as *;
|
||||
|
||||
// Loading Mixin for before/after pseudo element
|
||||
/// Loading pseudo-element styles for async content.
|
||||
@mixin loading {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
@use "sass:map";
|
||||
@use "sass:list";
|
||||
|
||||
/// Responsive breakpoints map for the `media` mixin.
|
||||
/// @type Map
|
||||
$breakpoints: (
|
||||
'xs': null,
|
||||
'sm': 680px,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
@use "theme-vars" as *;
|
||||
|
||||
/// Set scrollbar width CSS custom properties.
|
||||
/// @param {String} $width [$width: thin] - Scrollbar width value
|
||||
/// @param {Length} $widthLegacy [$widthLegacy: 12px] - Legacy scrollbar width
|
||||
@mixin scrollbar-width($width: thin, $widthLegacy: 12px) {
|
||||
@include set-fi-vars((
|
||||
scrollbar-width: $width,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
@use "variables" as *;
|
||||
|
||||
// Shared defaults for theme-switch icon motion.
|
||||
// Keep them overridable so other entries can reuse the same mixin with different pacing.
|
||||
/// Duration of the theme-switch icon animation.
|
||||
/// @type Time
|
||||
$theme-switch-animation-duration: 0.5s !default;
|
||||
|
||||
/// Easing function for the theme-switch icon animation.
|
||||
/// @type String
|
||||
$theme-switch-animation-easing: cubic-bezier(0.22, 1, 0.36, 1) !default;
|
||||
|
||||
/// Bind a theme mode selector to an icon glyph and its matching animation.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@use "sass:map";
|
||||
|
||||
/// Z-index scale for consistent layering across the theme.
|
||||
/// @type Map
|
||||
$z-indexes: (
|
||||
"hide": -1,
|
||||
"auto": auto,
|
||||
|
||||
@@ -676,7 +676,7 @@ wrapper = true
|
||||
cdn = "https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs"
|
||||
# ZenUML ESM module CDN source.
|
||||
# You can set it to a CDN source to enable ZenUML support.
|
||||
# Example: `https://cdn.jsdelivr.net/npm/@mermaid-js/mermaid-zenuml/dist/mermaid-zenuml.esm.min.mjs`.
|
||||
# Example: `https://cdn.jsdelivr.net/npm/@mermaid-js/mermaid-zenuml/dist/mermaid-zenuml.esm.min.mjs`
|
||||
zenuml = ""
|
||||
# For values, See: [Available Themes](https://mermaid.js.org/config/theming.html#available-themes).
|
||||
themes = [
|
||||
@@ -692,7 +692,7 @@ look = "handDrawn"
|
||||
# Specifies the font to be used in the rendered diagrams.
|
||||
font_family = ""
|
||||
# Layout loaders from ESM module CDN sources (e.g., ELK layout engine).
|
||||
# Example: ["https://cdn.jsdelivr.net/npm/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs"].
|
||||
# Example: ["https://cdn.jsdelivr.net/npm/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs"]
|
||||
# This will enable additional layout options: ["elk", "elk.layered", "elk.stress", "elk.force", "elk.mrtree"].
|
||||
layout_loaders = []
|
||||
# Default layout algorithm for rendering diagrams.
|
||||
@@ -859,9 +859,9 @@ user_desc = ""
|
||||
# DOM container to be blacked out, e.g. [".aplayer"]
|
||||
black_dom = []
|
||||
# only for iframe mode
|
||||
# Example: "375px".
|
||||
# Example: "375px"
|
||||
frame_width = ""
|
||||
# Example: "600px".
|
||||
# Example: "600px"
|
||||
frame_height = ""
|
||||
# Only for magic mode.
|
||||
user_icon = ""
|
||||
@@ -1163,21 +1163,21 @@ lazy_load = true
|
||||
[params.library]
|
||||
|
||||
# Load some CSS from local assets.
|
||||
# Example: someCSS = "css/some.css".
|
||||
# Example: `someCSS = "css/some.css"`
|
||||
# Load some CSS from remote CDN.
|
||||
# Example: someCSS = "https://cdn.example.com/some.css".
|
||||
# Example: `someCSS = "https://cdn.example.com/some.css"`
|
||||
[params.library.css]
|
||||
|
||||
# Load some JS from local assets.
|
||||
# Example: someJS = "js/some.js".
|
||||
# Example: `someJS = "js/some.js"`
|
||||
# Load some JS from remote CDN.
|
||||
# Example: someJS = "https://cdn.example.com/some.js".
|
||||
# Example: `someJS = "https://cdn.example.com/some.js"`
|
||||
[params.library.js]
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
# Public Git repository information only then enableGitInfo is true.
|
||||
[params.git_info]
|
||||
# Example: "https://github.com/hugo-fixit/docs".
|
||||
# Example: `"https://github.com/hugo-fixit/docs"`
|
||||
repo = ""
|
||||
branch = "main"
|
||||
# The content directory path relative to the root of the repository.
|
||||
@@ -1373,7 +1373,7 @@ enable = false
|
||||
animate = true
|
||||
icon = "fa-solid fa-heartbeat"
|
||||
pre = ""
|
||||
# Example: "2021-12-18T16:15:22+08:00".
|
||||
# Example: "2021-12-18T16:15:22+08:00"
|
||||
value = ""
|
||||
|
||||
# Footer lines order.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Loads the main stylesheet (every page) and conditionally loads
|
||||
page-specific CSS based on .Kind, .Layout, and .RelPermalink.
|
||||
|
||||
Called from: base/head/index.html
|
||||
Called from: `base/head/index.html`
|
||||
*/ -}}
|
||||
|
||||
{{- $scssVars := partialCached "function/scss-vars.html" . -}}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
@example
|
||||
// Practical usage in templates
|
||||
{{- $siteParams := partial "function/camel-case-keys.html" .Site.Params }}
|
||||
{{- $siteParams := partial "function/camel-case-keys.html" .Site.Params -}}
|
||||
*/ -}}
|
||||
{{- $input := . | default dict -}}
|
||||
{{- $output := dict -}}
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
@param {String} . - The snake_case string
|
||||
@return {String} The camelCase string
|
||||
|
||||
@example "max_shown_lines" -> "maxShownLines"
|
||||
@example
|
||||
// "max_shown_lines" -> "maxShownLines"
|
||||
{{- partial "function/camel-case.html" "max_shown_lines" -}}
|
||||
*/ -}}
|
||||
{{- $parts := split . "_" -}}
|
||||
{{- $result := index $parts 0 -}}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{{- /* Escape url special characters to query format, e.g: `#` -> `%23` */ -}}
|
||||
{{- /* https://github.com/hugo-fixit/FixIt/issues/245 */ -}}
|
||||
{{- /*
|
||||
Escape URL special characters to query format, e.g: `#` -> `%23`
|
||||
|
||||
See: https://github.com/hugo-fixit/FixIt/issues/245
|
||||
*/ -}}
|
||||
|
||||
{{- $content := . -}}
|
||||
{{- $content = replace $content "#" "%23" -}}
|
||||
|
||||
@@ -21,8 +21,9 @@
|
||||
|
||||
@example
|
||||
{{- $cover := dict "Page" . | partial "function/get-cover.html" -}}
|
||||
{{- with $cover.URL }}<img src="{{ . }}">{{ end -}}
|
||||
{{- with $cover.URL -}}<img src="{{ . }}">{{- end -}}
|
||||
|
||||
@example
|
||||
{{- $cover := dict "Page" . "Preview" true | partial "function/get-cover.html" -}}
|
||||
{{- dict "Src" $cover.URL "Resources" .Resources "Matches" $cover.Matches | partial "plugin/image.html" -}}
|
||||
*/ -}}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
{{- /*
|
||||
Example:
|
||||
{{- $suffixList := slice ".jpeg" ".jpg" ".png" ".gif" ".bmp" ".tif" ".tiff" ".webp" ".avif" ".svg" -}}
|
||||
{{- $suffixValid := (dict "Path" .Path "Suffixes" $suffixList | partial "function/suffix-validation.html") -}}
|
||||
{{- /*
|
||||
Validate if the given path has a valid suffix.
|
||||
@param Path string The path to validate.
|
||||
@param Suffixes []string The list of valid suffixes.
|
||||
@return bool True if the path has a valid suffix, false otherwise.
|
||||
|
||||
@example
|
||||
{{- $suffixList := slice ".jpeg" ".jpg" ".png" ".gif" ".bmp" ".tif" ".tiff" ".webp" ".avif" ".svg" -}}
|
||||
{{- $suffixValid := (dict "Path" .Path "Suffixes" $suffixList | partial "function/suffix-validation.html") -}}
|
||||
*/ -}}
|
||||
{{- $url := urls.Parse .Path -}}
|
||||
{{- $path := path.Clean ((printf "%v%v" $url.Host $url.Path) | lower) -}}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{{- /*
|
||||
Icon renderer for Font Awesome classes and SVG sources.
|
||||
- Renders <i> element when Class is provided.
|
||||
- Renders `<i>` element when `.Class` is provided.
|
||||
- Resolves local SVG resources or external SVG URL fallback.
|
||||
- Supports Simple Icons shorthand by icon name.
|
||||
@param {String} [.Class] - Icon class name for <i>
|
||||
@param {String} [.Class] - Icon class name for `<i>`
|
||||
@param {String} [.Src] - SVG source path or URL
|
||||
@param {String} [.Simpleicons] - Simple-icons icon name
|
||||
@param {String} [.Prefix] - Custom prefix for simple-icons source path
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{- /*
|
||||
Social profile link helper.
|
||||
- Builds destination from explicit Url or from Prefix/Template/Id.
|
||||
- Builds destination from explicit URL or from Prefix/Template/Id.
|
||||
- Delegates final rendering to plugin/link.html with rel=me.
|
||||
@param {String} [.Url] - Explicit target URL
|
||||
@param {String} [.Template] - URL template containing one %%v placeholder
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{- /*
|
||||
Stylesheet tag renderer.
|
||||
- Accepts direct <link> HTML, a pre-built resource, or builds from source path.
|
||||
- Accepts direct `<link>` HTML, a pre-built resource, or builds from source path.
|
||||
- Supports template execution, optional toCSS, minify/fingerprint, and preload mode.
|
||||
@param {String} [.Source] - Stylesheet URL/path; if starts with "<link" it's treated as raw HTML
|
||||
@param {resource.Resource} [.Resource] - Pre-built resource (skips build pipeline)
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
"version": "pnpm -F versioning start prod",
|
||||
"changelog": "auto-changelog-plus --starting-date 2021-12-18",
|
||||
"gen:lexers": "pnpm -F chroma-lexers start",
|
||||
"gen:docs": "pnpm -F gen-docs start",
|
||||
"gen:typedoc": "typedoc",
|
||||
"gen:sassdoc": "npx sassdoc@2.7.2 assets/scss/ --dest ../fixit-docs/static/references/scss --config .sassdocrc",
|
||||
"unocss": "unocss",
|
||||
"unocss:watch": "unocss --watch",
|
||||
"prepare": "simple-git-hooks"
|
||||
@@ -61,6 +64,7 @@
|
||||
"serve": "^14.2.6",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"tsx": "^4.22.4",
|
||||
"typedoc": "^0.28.20",
|
||||
"typescript": "^6.0.3",
|
||||
"unocss": "^66.7.4"
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@ export function generateScss(lexers: LexerEntry[]): void {
|
||||
const lines: string[] = [
|
||||
'/// Map of code identifiers/languages to Chroma supported languages.',
|
||||
'/// This file is auto-generated by `pnpm gen:lexers`, do not edit manually.',
|
||||
'/// @see https://github.com/hugo-fixit/FixIt/tree/main/packages/chroma-lexers',
|
||||
'/// See: https://github.com/hugo-fixit/FixIt/tree/main/packages/chroma-lexers',
|
||||
'$chroma-lexers: (',
|
||||
]
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Then in your `package.json`:
|
||||
{
|
||||
"scripts": {
|
||||
"build": "hugo --gc --minify --logLevel info",
|
||||
"postbuild": "fixit-encrypt && fixit-encrypt --verify"
|
||||
"postbuild": "fixit-encrypt"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# @hugo-fixit/gen-docs
|
||||
|
||||
Generate API reference documentation for the FixIt Hugo theme from source code.
|
||||
|
||||
- **Config**: parses `hugo.toml` to produce Markdown configuration reference.
|
||||
- **Partials**: parses `layouts/_partials/` to produce Markdown Hugo partials reference.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Generate all docs (config + partials)
|
||||
pnpm gen:docs
|
||||
|
||||
# Config subcommand
|
||||
pnpm gen:docs config
|
||||
pnpm gen:docs config hugo.toml -o /path/to/config.md
|
||||
pnpm gen:docs config latest
|
||||
pnpm gen:docs config https://example.com/hugo.toml
|
||||
pnpm gen:docs config hugo.toml -t template.md
|
||||
|
||||
# Partials subcommand
|
||||
pnpm gen:docs partials
|
||||
pnpm gen:docs partials -t template.md
|
||||
pnpm gen:docs partials -o output.md
|
||||
```
|
||||
|
||||
## Subcommands
|
||||
|
||||
### `config`
|
||||
|
||||
Generates configuration reference from `hugo.toml`. The input source can be:
|
||||
|
||||
- **Local file** (default): `pnpm gen:docs config [path]`
|
||||
- **URL**: `pnpm gen:docs config https://example.com/hugo.toml`
|
||||
- **Latest release**: `pnpm gen:docs config latest` (fetches from GitHub releases)
|
||||
|
||||
Options:
|
||||
|
||||
- `-o, --output <file>` — Write output to file (default: stdout)
|
||||
- `-t, --template <file>` — Inject docs between `<!-- HUGO_FIXIT_PARAMS:START -->` and `<!-- HUGO_FIXIT_PARAMS:END -->` markers in the template file
|
||||
|
||||
### `partials`
|
||||
|
||||
Generates Hugo partials reference from `layouts/_partials/`.
|
||||
|
||||
Options:
|
||||
|
||||
- `-o, --output <file>` — Write output to file (default: stdout)
|
||||
- `-t, --template <file>` — Inject docs between `<!-- HUGO_FIXIT_PARTIALS:START -->` and `<!-- HUGO_FIXIT_PARTIALS:END -->` markers in the template file
|
||||
|
||||
## Default Output
|
||||
|
||||
When run without a subcommand, generates both config and partials docs (injects into `en` docs templates).
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
src/
|
||||
index.ts # CLI entry point (commander)
|
||||
parsers/
|
||||
config-parser.ts # TOML parser for hugo.toml
|
||||
partial-parser.ts # HTML comment parser for layouts/_partials/
|
||||
renderers/
|
||||
config.ts # -> config.md
|
||||
partials.ts # -> partials.md
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@hugo-fixit/gen-docs",
|
||||
"private": true,
|
||||
"description": "Generate API reference documentation from FixIt source files",
|
||||
"author": "Lruihao",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "tsx src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hugo-fixit/shared": "workspace:*",
|
||||
"commander": "^14.0.3",
|
||||
"picocolors": "^1.1.1",
|
||||
"toml": "^3.0.0",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { consola, fromRoot } from '@hugo-fixit/shared'
|
||||
import { Command } from 'commander'
|
||||
import c from 'picocolors'
|
||||
import { applyTemplate, generateDocs } from './parsers/config-parser'
|
||||
import { parsePartials } from './parsers/partial-parser'
|
||||
import { renderConfigStandalone } from './renderers/config'
|
||||
import { generatePartialsDocs } from './renderers/partials'
|
||||
|
||||
function writeFile(filePath: string, content: string): void {
|
||||
fs.writeFileSync(filePath, content, 'utf-8')
|
||||
consola.success(`Written ${c.cyan(filePath)}`)
|
||||
}
|
||||
|
||||
// ─── Partials helpers ───
|
||||
|
||||
const PARTIALS_START = '<!-- HUGO_FIXIT_PARTIALS:START -->'
|
||||
const PARTIALS_END = '<!-- HUGO_FIXIT_PARTIALS:END -->'
|
||||
|
||||
function applyPartialsTemplate(body: string, templatePath: string, outputPath?: string, command?: string): void {
|
||||
const template = fs.readFileSync(templatePath, 'utf-8')
|
||||
const startIdx = template.indexOf(PARTIALS_START)
|
||||
const endIdx = template.indexOf(PARTIALS_END)
|
||||
|
||||
if (startIdx === -1 || endIdx === -1) {
|
||||
throw new Error(`Markers not found in template file. Expected both "${PARTIALS_START}" and "${PARTIALS_END}".`)
|
||||
}
|
||||
if (startIdx > endIdx) {
|
||||
throw new Error('START marker appears after END marker in template file.')
|
||||
}
|
||||
|
||||
const before = template.slice(0, startIdx + PARTIALS_START.length)
|
||||
const after = template.slice(endIdx)
|
||||
const comment = command
|
||||
? `<!--\nAutomatically generated by the \`${command}\` command.\nDo not modify it manually!\n-->\n\n`
|
||||
: ''
|
||||
const result = `${before}\n${comment}${body}\n${after}`
|
||||
|
||||
const dest = outputPath || templatePath
|
||||
fs.writeFileSync(dest, result, 'utf-8')
|
||||
}
|
||||
|
||||
function generatePartialsAction(options: { output?: string, template?: string }): void {
|
||||
const groups = parsePartials()
|
||||
const body = generatePartialsDocs(groups)
|
||||
|
||||
if (options.template) {
|
||||
const templatePath = path.resolve(process.cwd(), options.template)
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
consola.error(`Template not found: ${templatePath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
const outputPath = options.output ? path.resolve(process.cwd(), options.output) : undefined
|
||||
let cmd = `pnpm gen:docs partials -t ${options.template}`
|
||||
if (options.output)
|
||||
cmd += ` -o ${options.output}`
|
||||
applyPartialsTemplate(body, templatePath, outputPath, cmd)
|
||||
const dest = outputPath || templatePath
|
||||
consola.success(`Partials docs applied: ${c.cyan(dest)}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (options.output) {
|
||||
const outPath = path.resolve(process.cwd(), options.output)
|
||||
fs.writeFileSync(outPath, body, 'utf-8')
|
||||
consola.success(`Written ${c.cyan(outPath)}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Default: inject into en docs template
|
||||
const templatePath = fromRoot('../fixit-docs/content/en/references/partials/index.md')
|
||||
applyPartialsTemplate(body, templatePath, undefined, 'pnpm gen:docs')
|
||||
}
|
||||
|
||||
// ─── Subcommands ───
|
||||
|
||||
async function configAction(
|
||||
file: string,
|
||||
options: { output?: string, template?: string },
|
||||
): Promise<void> {
|
||||
let source = file
|
||||
let content: string
|
||||
|
||||
if (source === 'latest') {
|
||||
consola.start('Resolving latest FixIt release version...')
|
||||
try {
|
||||
const res = await fetch('https://api.github.com/repos/hugo-fixit/FixIt/releases/latest', {
|
||||
headers: {
|
||||
'User-Agent': 'gen-docs',
|
||||
...(process.env.GITHUB_TOKEN && { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }),
|
||||
},
|
||||
})
|
||||
if (!res.ok)
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
const { tag_name: version } = await res.json() as { tag_name: string }
|
||||
source = `https://raw.githubusercontent.com/hugo-fixit/FixIt/refs/tags/${version}/hugo.toml`
|
||||
consola.success(`Resolved latest release: ${c.cyan(version)}`)
|
||||
}
|
||||
catch (error) {
|
||||
consola.error(`Failed to resolve latest release: ${(error as Error).message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(source)) {
|
||||
consola.start(`Downloading from: ${c.cyan(source)}`)
|
||||
try {
|
||||
const res = await fetch(source)
|
||||
if (!res.ok)
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
content = await res.text()
|
||||
consola.success('Remote file downloaded.')
|
||||
}
|
||||
catch (error) {
|
||||
consola.error(`Failed to download: ${(error as Error).message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
else {
|
||||
const inputPath = source === 'hugo.toml' ? fromRoot('hugo.toml') : path.resolve(process.cwd(), source)
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
consola.error(`File not found: ${inputPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
content = fs.readFileSync(inputPath, 'utf-8')
|
||||
}
|
||||
|
||||
if (options.template) {
|
||||
const templatePath = path.resolve(process.cwd(), options.template)
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
consola.error(`Template file not found: ${templatePath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
const outputPath = options.output ? path.resolve(process.cwd(), options.output) : undefined
|
||||
let cmd = `gen-docs config ${file}`
|
||||
if (options.template)
|
||||
cmd += ` -t ${options.template}`
|
||||
if (options.output)
|
||||
cmd += ` -o ${options.output}`
|
||||
applyTemplate(content, templatePath, outputPath, cmd)
|
||||
const dest = outputPath || templatePath
|
||||
consola.success(`Documentation applied to template: ${c.cyan(dest)}`)
|
||||
return
|
||||
}
|
||||
|
||||
const body = generateDocs(content)
|
||||
if (options.output) {
|
||||
const outPath = path.resolve(process.cwd(), options.output)
|
||||
writeFile(outPath, renderConfigStandalone(body))
|
||||
}
|
||||
else {
|
||||
process.stdout.write(`${body}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
function allAction(): void {
|
||||
// Config API — inject into docs params page
|
||||
consola.start('Parsing hugo.toml...')
|
||||
const tomlContent = fs.readFileSync(fromRoot('hugo.toml'), 'utf-8')
|
||||
const templatePath = fromRoot('../fixit-docs/content/en/documentation/getting-started/configuration/params/index.md')
|
||||
applyTemplate(tomlContent, templatePath, undefined, 'pnpm gen:docs')
|
||||
consola.success('Config docs generated!')
|
||||
|
||||
// Partials API — inject into en docs partials page
|
||||
consola.start('Generating Hugo partials docs...')
|
||||
generatePartialsAction({})
|
||||
consola.success('Partials docs generated!')
|
||||
}
|
||||
|
||||
// ─── CLI ───
|
||||
|
||||
const program = new Command()
|
||||
|
||||
program
|
||||
.name('gen-docs')
|
||||
.description('Generate API reference documentation for the FixIt Hugo theme')
|
||||
|
||||
program
|
||||
.command('config')
|
||||
.description('Generate configuration reference from hugo.toml')
|
||||
.argument('[file]', 'Input hugo.toml path, URL, or "latest"', 'hugo.toml')
|
||||
.option('-o, --output <file>', 'Output file path (default: stdout)')
|
||||
.option('-t, --template <file>', 'Template file with HUGO_FIXIT_PARAMS markers to inject docs into')
|
||||
.action(configAction)
|
||||
|
||||
program
|
||||
.command('partials')
|
||||
.description('Generate Hugo partials reference from layouts/_partials/')
|
||||
.option('-o, --output <file>', 'Output file path (default: stdout)')
|
||||
.option('-t, --template <file>', 'Template file with HUGO_FIXIT_PARTIALS markers to inject docs into')
|
||||
.action(generatePartialsAction)
|
||||
|
||||
// Default action (no subcommand): inject config + partials docs into docs pages
|
||||
program
|
||||
.action(() => allAction())
|
||||
|
||||
program.parse()
|
||||
@@ -0,0 +1,568 @@
|
||||
import fs from 'node:fs'
|
||||
import { fromRoot } from '@hugo-fixit/shared'
|
||||
import toml from 'toml'
|
||||
|
||||
interface ParamDoc {
|
||||
name: string
|
||||
type: string
|
||||
defaultValue: string
|
||||
description: string
|
||||
children: ParamDoc[]
|
||||
isTable: boolean
|
||||
isArray: boolean
|
||||
rawValue: unknown
|
||||
}
|
||||
|
||||
function isSeparatorComment(text: string): boolean {
|
||||
return /^[-=+]{3,}$/.test(text.trim())
|
||||
|| /^[-=]{20,}\s*$/.test(text.trim())
|
||||
|| /^\+{20,}\s*$/.test(text.trim())
|
||||
|| /\s+#$/.test(text.trim())
|
||||
}
|
||||
|
||||
function isHardSeparator(text: string): boolean {
|
||||
return /^[-=]{20,}\s*$/.test(text.trim())
|
||||
|| /^\+{20,}\s*$/.test(text.trim())
|
||||
}
|
||||
|
||||
function extractComments(content: string): { commentMap: Map<string, string[]>, sectionCommentMap: Map<string, string[]>, pageLevelStartKey: string, pageLevelEndKey: string } {
|
||||
const commentMap = new Map<string, string[]>()
|
||||
const sectionCommentMap = new Map<string, string[]>()
|
||||
const lines = content.split('\n')
|
||||
const pendingComments: string[] = []
|
||||
const sectionStack: string[] = []
|
||||
|
||||
let inParams = false
|
||||
let sectionCommentCount = 0
|
||||
let afterHardSeparator = false
|
||||
let pageLevelStartKey = ''
|
||||
let pageLevelEndKey = ''
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const trimmed = line.trim()
|
||||
|
||||
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/)
|
||||
if (sectionMatch) {
|
||||
const key = sectionMatch[1]
|
||||
if (key === 'params') {
|
||||
inParams = true
|
||||
sectionStack.length = 0
|
||||
sectionStack.push('params')
|
||||
pendingComments.length = 0
|
||||
sectionCommentCount = 0
|
||||
continue
|
||||
}
|
||||
if (inParams && key.startsWith('params.')) {
|
||||
if (afterHardSeparator && !pageLevelEndKey && pageLevelStartKey) {
|
||||
pageLevelEndKey = key.split('.')[1]
|
||||
afterHardSeparator = false
|
||||
}
|
||||
// Collect section-level comments for empty sections.
|
||||
// Walk backward from the section header and collect contiguous comment lines.
|
||||
// Skip blank lines between the section header and comments.
|
||||
const sectionPath = key.split('.').slice(1).join('.')
|
||||
let scanIdx = i - 1
|
||||
const sectionComments: string[] = []
|
||||
// Skip blank lines immediately before the section header
|
||||
let hasBlankBeforeComments = false
|
||||
while (scanIdx >= 0 && lines[scanIdx].trim() === '') {
|
||||
hasBlankBeforeComments = true
|
||||
scanIdx--
|
||||
}
|
||||
// Collect comment lines
|
||||
while (scanIdx >= 0 && lines[scanIdx].trim().startsWith('#')) {
|
||||
sectionComments.unshift(lines[scanIdx].trim().replace(/^#\s?/, ''))
|
||||
scanIdx--
|
||||
}
|
||||
if (sectionComments.length > 0) {
|
||||
const prevIsSection = scanIdx >= 0 && /^\[[^\]]+\]$/.test(lines[scanIdx].trim())
|
||||
// Key rule: if there's NO blank line between the section header and the
|
||||
// comments above it, the comments are section-level for this section.
|
||||
// If there IS a blank line, the comments are trailing content from the
|
||||
// previous section (e.g., examples) and should not be assigned here.
|
||||
if (!hasBlankBeforeComments && prevIsSection) {
|
||||
sectionCommentMap.set(sectionPath, sectionComments)
|
||||
}
|
||||
else if (!prevIsSection) {
|
||||
// Comments preceded by non-section content or start-of-file
|
||||
sectionCommentMap.set(sectionPath, sectionComments)
|
||||
}
|
||||
}
|
||||
let splitIdx = pendingComments.length
|
||||
let foundSplitPoint = false
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
const prevLine = lines[j].trim()
|
||||
if (prevLine === '') {
|
||||
foundSplitPoint = true
|
||||
break
|
||||
}
|
||||
if (prevLine.startsWith('#')) {
|
||||
splitIdx--
|
||||
continue
|
||||
}
|
||||
foundSplitPoint = true
|
||||
break
|
||||
}
|
||||
if (foundSplitPoint && splitIdx > 0 && sectionStack.length > 1) {
|
||||
const sectionKey = sectionStack.slice(1).join('.')
|
||||
commentMap.set(sectionKey, pendingComments.slice(0, splitIdx))
|
||||
}
|
||||
const trailing = pendingComments.slice(splitIdx)
|
||||
pendingComments.length = 0
|
||||
pendingComments.push(...trailing)
|
||||
sectionCommentCount = trailing.length
|
||||
const parts = key.split('.')
|
||||
sectionStack.length = 0
|
||||
sectionStack.push(...parts)
|
||||
continue
|
||||
}
|
||||
if (inParams) {
|
||||
const rootKey = key.split('.')[0]
|
||||
const nonParamsSections = ['menus', 'markup', 'outputs', 'taxonomies', 'related', 'frontmatter', 'imaging', 'module', 'sitemap', 'minify', 'caches', 'security']
|
||||
if (nonParamsSections.includes(rootKey)) {
|
||||
inParams = false
|
||||
sectionStack.length = 0
|
||||
pendingComments.length = 0
|
||||
continue
|
||||
}
|
||||
let splitIdx = pendingComments.length
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
const prevLine = lines[j].trim()
|
||||
if (prevLine === '')
|
||||
break
|
||||
if (prevLine.startsWith('#')) {
|
||||
splitIdx--
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if (splitIdx > 0 && sectionStack.length > 1) {
|
||||
const sectionKey = sectionStack.slice(1).join('.')
|
||||
commentMap.set(sectionKey, pendingComments.slice(0, splitIdx))
|
||||
}
|
||||
const trailing = pendingComments.slice(splitIdx)
|
||||
pendingComments.length = 0
|
||||
pendingComments.push(...trailing)
|
||||
sectionStack.length = Math.min(sectionStack.length, 1)
|
||||
sectionStack.push(key)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (!inParams)
|
||||
continue
|
||||
|
||||
if (trimmed.startsWith('#')) {
|
||||
const commentText = trimmed.replace(/^#\s?/, '')
|
||||
if (isSeparatorComment(commentText)) {
|
||||
if (isHardSeparator(commentText)) {
|
||||
pendingComments.length = 0
|
||||
afterHardSeparator = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
pendingComments.push(commentText)
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmed === '') {
|
||||
continue
|
||||
}
|
||||
|
||||
const kvMatch = trimmed.match(/^(\w+)\s*=/)
|
||||
if (kvMatch && sectionStack.length > 0) {
|
||||
const keyName = kvMatch[1]
|
||||
const fullPath = [...sectionStack.slice(1), keyName].join('.')
|
||||
if (afterHardSeparator && !pageLevelStartKey && sectionStack.length === 1) {
|
||||
pageLevelStartKey = keyName
|
||||
afterHardSeparator = false
|
||||
}
|
||||
if (sectionStack.length > 1 && pendingComments.length > 0) {
|
||||
const sectionKey = sectionStack.slice(1).join('.')
|
||||
if (!commentMap.has(sectionKey) && sectionCommentCount > 0) {
|
||||
commentMap.set(sectionKey, pendingComments.slice(0, sectionCommentCount))
|
||||
pendingComments.splice(0, sectionCommentCount)
|
||||
}
|
||||
sectionCommentCount = 0
|
||||
}
|
||||
if (pendingComments.length > 0) {
|
||||
commentMap.set(fullPath, [...pendingComments])
|
||||
pendingComments.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
const arrayMatch = trimmed.match(/^\[\[(\w+(?:\.\w+)*)\]\]$/)
|
||||
if (arrayMatch && inParams) {
|
||||
const keyPath = arrayMatch[1]
|
||||
const fullPath = [...sectionStack.slice(1), keyPath].join('.')
|
||||
if (pendingComments.length > 0) {
|
||||
commentMap.set(fullPath, [...pendingComments])
|
||||
pendingComments.length = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { commentMap, sectionCommentMap, pageLevelStartKey, pageLevelEndKey }
|
||||
}
|
||||
|
||||
function detectType(value: unknown): string {
|
||||
if (value === null || value === undefined)
|
||||
return 'string'
|
||||
if (typeof value === 'boolean')
|
||||
return 'bool'
|
||||
if (typeof value === 'number')
|
||||
return Number.isInteger(value) ? 'int' : 'float'
|
||||
if (typeof value === 'string')
|
||||
return 'string'
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0)
|
||||
return 'string array'
|
||||
return `${detectType(value[0])} array`
|
||||
}
|
||||
if (typeof value === 'object')
|
||||
return 'map'
|
||||
return 'string'
|
||||
}
|
||||
|
||||
function formatDefault(value: unknown): string {
|
||||
if (value === null || value === undefined)
|
||||
return ''
|
||||
if (typeof value === 'string')
|
||||
return value === '' ? '`""`' : `\`${JSON.stringify(value)}\``
|
||||
if (typeof value === 'boolean')
|
||||
return `\`${value}\``
|
||||
if (typeof value === 'number')
|
||||
return `\`${value}\``
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0)
|
||||
return '`[]`'
|
||||
return `\`${JSON.stringify(value)}\``
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function formatTomlValue(value: unknown): string {
|
||||
if (value === null || value === undefined)
|
||||
return '""'
|
||||
if (typeof value === 'string')
|
||||
return JSON.stringify(value)
|
||||
if (typeof value === 'boolean')
|
||||
return String(value)
|
||||
if (typeof value === 'number')
|
||||
return String(value)
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0)
|
||||
return '[]'
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
return '""'
|
||||
}
|
||||
|
||||
function isExampleLine(line: string): boolean {
|
||||
return /^\s*(?:"[^"]*"|'[^']*'|[\w.-]+)\s*=\s*.+/.test(line)
|
||||
}
|
||||
|
||||
function joinComments(comments: string[]): string {
|
||||
if (comments.length === 0)
|
||||
return ''
|
||||
const filtered = comments.filter(c => c.trim() !== '')
|
||||
if (filtered.length === 0)
|
||||
return ''
|
||||
const sentences = filtered.map((line) => {
|
||||
const trimmed = line.trim()
|
||||
if (isExampleLine(trimmed)) {
|
||||
return `Example: \`${trimmed}\`.`
|
||||
}
|
||||
let s = trimmed
|
||||
if (!s.startsWith('http://') && !s.startsWith('https://')) {
|
||||
s = s.charAt(0).toUpperCase() + s.slice(1)
|
||||
}
|
||||
if (!s.endsWith('.') && !s.endsWith('!') && !s.endsWith('?'))
|
||||
s += '.'
|
||||
return s
|
||||
})
|
||||
return sentences.join(' ')
|
||||
}
|
||||
|
||||
function documentSection(
|
||||
obj: Record<string, unknown>,
|
||||
commentMap: Map<string, string[]>,
|
||||
parentPath: string,
|
||||
sectionCommentMap?: Map<string, string[]>,
|
||||
): ParamDoc[] {
|
||||
const docs: ParamDoc[] = []
|
||||
const seenKeys = new Set<string>()
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const fullPath = parentPath ? `${parentPath}.${key}` : key
|
||||
const comments = commentMap.get(fullPath) || []
|
||||
// Fall back to section-level comments if no key-level comments
|
||||
const sectionComments = sectionCommentMap?.get(fullPath)
|
||||
const effectiveComments = comments.length > 0 ? comments : (sectionComments || [])
|
||||
const type = detectType(value)
|
||||
const isTable = typeof value === 'object' && !Array.isArray(value) && value !== null
|
||||
const isArray = Array.isArray(value)
|
||||
|
||||
const doc: ParamDoc = {
|
||||
name: key,
|
||||
type,
|
||||
defaultValue: formatDefault(value),
|
||||
description: joinComments(effectiveComments),
|
||||
children: [],
|
||||
isTable,
|
||||
isArray,
|
||||
rawValue: value,
|
||||
}
|
||||
|
||||
if (isTable && !isArray) {
|
||||
doc.children = documentSection(
|
||||
value as Record<string, unknown>,
|
||||
commentMap,
|
||||
fullPath,
|
||||
sectionCommentMap,
|
||||
)
|
||||
}
|
||||
|
||||
docs.push(doc)
|
||||
seenKeys.add(key)
|
||||
}
|
||||
|
||||
// Add placeholder entries for empty sections that have section-level comments
|
||||
if (sectionCommentMap) {
|
||||
for (const [sectionPath, comments] of sectionCommentMap) {
|
||||
const prefix = parentPath ? `${parentPath}.` : ''
|
||||
if (sectionPath.startsWith(prefix) && !sectionPath.slice(prefix.length).includes('.')) {
|
||||
const key = sectionPath.slice(prefix.length)
|
||||
if (key && !seenKeys.has(key)) {
|
||||
docs.push({
|
||||
name: key,
|
||||
type: 'map',
|
||||
defaultValue: '',
|
||||
description: joinComments(comments),
|
||||
children: [],
|
||||
isTable: true,
|
||||
isArray: false,
|
||||
rawValue: {},
|
||||
})
|
||||
seenKeys.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return docs
|
||||
}
|
||||
|
||||
function rawDefault(doc: ParamDoc): string {
|
||||
const val = doc.defaultValue.replace(/`/g, '')
|
||||
return val || '""'
|
||||
}
|
||||
|
||||
function renderTomlBlock(doc: ParamDoc, lines: string[], sectionPath: string): void {
|
||||
lines.push(`[params.${sectionPath}]`)
|
||||
|
||||
for (const child of doc.children) {
|
||||
if (child.isTable) {
|
||||
lines.push('')
|
||||
lines.push(`[params.${sectionPath}.${child.name}]`)
|
||||
for (const grandchild of child.children) {
|
||||
if (grandchild.isTable) {
|
||||
lines.push('')
|
||||
lines.push(`[params.${sectionPath}.${child.name}.${grandchild.name}]`)
|
||||
for (const gg of grandchild.children) {
|
||||
lines.push(`${gg.name} = ${rawDefault(gg)}`)
|
||||
}
|
||||
}
|
||||
else {
|
||||
lines.push(`${grandchild.name} = ${rawDefault(grandchild)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (child.isArray && Array.isArray(child.rawValue) && child.rawValue.length > 0 && typeof child.rawValue[0] === 'object') {
|
||||
for (const item of child.rawValue) {
|
||||
lines.push('')
|
||||
lines.push(`[[params.${sectionPath}.${child.name}]]`)
|
||||
for (const [k, v] of Object.entries(item as Record<string, unknown>)) {
|
||||
lines.push(`${k} = ${formatTomlValue(v)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
lines.push(`${child.name} = ${rawDefault(child)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderChildren(children: ParamDoc[], lines: string[]): void {
|
||||
for (const child of children) {
|
||||
let meta = `\`${child.type}\``
|
||||
const desc = child.description.replace(/\.$/, '')
|
||||
if (child.defaultValue && child.type !== 'map') {
|
||||
meta += desc ? ` ${desc}. Default is ${child.defaultValue}.` : ` Default is ${child.defaultValue}.`
|
||||
}
|
||||
else if (desc) {
|
||||
meta += ` ${desc}.`
|
||||
}
|
||||
|
||||
lines.push(child.name)
|
||||
lines.push(`: ${meta}`)
|
||||
lines.push('')
|
||||
|
||||
if (child.children.length > 0) {
|
||||
for (const grandchild of child.children) {
|
||||
let gcMeta = `\`${grandchild.type}\``
|
||||
const gcDesc = grandchild.description.replace(/\.$/, '')
|
||||
if (grandchild.defaultValue && grandchild.type !== 'map') {
|
||||
gcMeta += gcDesc ? ` ${gcDesc}. Default is ${grandchild.defaultValue}.` : ` Default is ${grandchild.defaultValue}.`
|
||||
}
|
||||
else if (gcDesc) {
|
||||
gcMeta += ` ${gcDesc}.`
|
||||
}
|
||||
lines.push(`- ${grandchild.name}: ${gcMeta}`)
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderParam(doc: ParamDoc): string {
|
||||
const lines: string[] = []
|
||||
|
||||
let meta = `\`${doc.type}\``
|
||||
const desc = doc.description.replace(/\.$/, '')
|
||||
if (doc.defaultValue && doc.type !== 'map') {
|
||||
meta += desc ? ` ${desc}. Default is ${doc.defaultValue}.` : ` Default is ${doc.defaultValue}.`
|
||||
}
|
||||
else if (desc) {
|
||||
meta += ` ${desc}.`
|
||||
}
|
||||
|
||||
lines.push(`### ${doc.name}`)
|
||||
lines.push('')
|
||||
lines.push(meta)
|
||||
|
||||
if (doc.children.length > 0) {
|
||||
lines.push('')
|
||||
|
||||
const hasContent = (children: ParamDoc[]): boolean =>
|
||||
children.some(child => !child.isTable || (child.children.length > 0 && hasContent(child.children)))
|
||||
const codeBlock = hasContent(doc.children) ? '```toggle' : '```toml'
|
||||
|
||||
lines.push(codeBlock)
|
||||
lines.push('[params]')
|
||||
lines.push('')
|
||||
renderTomlBlock(doc, lines, doc.name)
|
||||
lines.push('```')
|
||||
lines.push('')
|
||||
|
||||
renderChildren(doc.children, lines)
|
||||
}
|
||||
|
||||
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
||||
lines.pop()
|
||||
}
|
||||
return lines.map(line => line.trimEnd()).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate theme configuration documentation from TOML content string.
|
||||
* @param content TOML content string
|
||||
* @returns Markdown documentation body (no frontmatter)
|
||||
*/
|
||||
export function generateDocs(content: string): string {
|
||||
const parsed = toml.parse(content)
|
||||
|
||||
if (!parsed.params) {
|
||||
throw new Error('No [params] section found in hugo.toml.')
|
||||
}
|
||||
|
||||
const { commentMap, sectionCommentMap, pageLevelStartKey, pageLevelEndKey } = extractComments(content)
|
||||
const params = parsed.params as Record<string, unknown>
|
||||
const docs = documentSection(params, commentMap, '', sectionCommentMap)
|
||||
|
||||
const siteLevel: ParamDoc[] = []
|
||||
const pageLevel: ParamDoc[] = []
|
||||
let isPageLevel = false
|
||||
|
||||
for (const doc of docs) {
|
||||
if (pageLevelStartKey && doc.name === pageLevelStartKey) {
|
||||
isPageLevel = true
|
||||
}
|
||||
if (pageLevelEndKey && doc.name === pageLevelEndKey) {
|
||||
isPageLevel = false
|
||||
}
|
||||
if (isPageLevel) {
|
||||
pageLevel.push(doc)
|
||||
}
|
||||
else {
|
||||
siteLevel.push(doc)
|
||||
}
|
||||
}
|
||||
|
||||
const output: string[] = []
|
||||
|
||||
if (siteLevel.length > 0) {
|
||||
output.push('## Site Level')
|
||||
output.push('These apply to the entire site and cannot be overridden on a per-page basis.')
|
||||
for (const doc of siteLevel) {
|
||||
output.push(renderParam(doc))
|
||||
}
|
||||
}
|
||||
|
||||
if (pageLevel.length > 0) {
|
||||
output.push('## Page Level')
|
||||
output.push('These can be overridden on a per-page basis via front matter.')
|
||||
for (const doc of pageLevel) {
|
||||
output.push(renderParam(doc))
|
||||
}
|
||||
}
|
||||
|
||||
return output.join('\n\n').split('\n').map(line => line.trimEnd()).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: read hugo.toml from the FixIt repo root and generate docs.
|
||||
*/
|
||||
export function parseConfig(): string {
|
||||
const content = fs.readFileSync(fromRoot('hugo.toml'), 'utf-8')
|
||||
return generateDocs(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply generated docs into a template file between markers.
|
||||
* Replaces content between `<!-- HUGO_FIXIT_PARAMS:START -->` and `<!-- HUGO_FIXIT_PARAMS:END -->`.
|
||||
* @param tomlContent TOML content string
|
||||
* @param templatePath Path to the template file
|
||||
* @param outputPath Optional output path (defaults to overwriting the template file)
|
||||
* @param command The CLI command that was used to generate the docs
|
||||
*/
|
||||
export function applyTemplate(tomlContent: string, templatePath: string, outputPath?: string, command?: string): void {
|
||||
const template = fs.readFileSync(templatePath, 'utf-8')
|
||||
const docs = generateDocs(tomlContent)
|
||||
|
||||
const startMarker = '<!-- HUGO_FIXIT_PARAMS:START -->'
|
||||
const endMarker = '<!-- HUGO_FIXIT_PARAMS:END -->'
|
||||
|
||||
const startIdx = template.indexOf(startMarker)
|
||||
const endIdx = template.indexOf(endMarker)
|
||||
|
||||
if (startIdx === -1 || endIdx === -1) {
|
||||
throw new Error(`Markers not found in template file. Expected both "${startMarker}" and "${endMarker}".`)
|
||||
}
|
||||
|
||||
if (startIdx > endIdx) {
|
||||
throw new Error('START marker appears after END marker in template file.')
|
||||
}
|
||||
|
||||
const before = template.slice(0, startIdx + startMarker.length)
|
||||
const after = template.slice(endIdx)
|
||||
const comment = command
|
||||
? `<!--\nAutomatically generated by the \`${command}\` command.\nDo not modify it manually!\n-->\n\n`
|
||||
: ''
|
||||
const result = `${before}\n${comment}${docs}\n${after}`
|
||||
|
||||
const dest = outputPath || templatePath
|
||||
fs.writeFileSync(dest, result, 'utf-8')
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fromRoot } from '@hugo-fixit/shared'
|
||||
|
||||
export interface PartialParam {
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
children: PartialParam[]
|
||||
}
|
||||
|
||||
export interface PartialDoc {
|
||||
name: string
|
||||
path: string
|
||||
description: string
|
||||
params: PartialParam[]
|
||||
returns: string
|
||||
examples: string[]
|
||||
}
|
||||
|
||||
export interface PartialGroup {
|
||||
name: string
|
||||
description: string
|
||||
partials: PartialDoc[]
|
||||
}
|
||||
|
||||
const GROUP_DESCRIPTIONS: Record<string, string> = {
|
||||
_debug: 'Debug utilities for development.',
|
||||
base: 'Core layout partials (header, footer, breadcrumb, paginator, comment, widgets, assets).',
|
||||
feed: 'RSS feed generation.',
|
||||
function: 'Reusable utility and helper function partials.',
|
||||
gen: 'Generated or config output partials.',
|
||||
home: 'Homepage-specific partials.',
|
||||
init: 'Theme initialization partials (version, environment detection, compatibility, global setup).',
|
||||
plugin: 'Third-party plugin integration partials.',
|
||||
section: 'Section-level partials.',
|
||||
single: 'Single-post page partials.',
|
||||
store: 'Asset accumulation store partials.',
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the leading Hugo comment block from file content.
|
||||
*/
|
||||
function extractComment(content: string): string | null {
|
||||
const match = content.match(/^\{\{-?\s*\/\*[\s\S]*?\*\/\s*-?\}\}/)
|
||||
if (!match)
|
||||
return null
|
||||
// Strip the delimiters: {{- /* at start, */ -}} at end
|
||||
const inner = match[0]
|
||||
.replace(/^\{\{-?\s*\/\*\s*/, '')
|
||||
.replace(/\s*\*\/\s*-?\}\}$/, '')
|
||||
return inner.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a comment block into sections by @tags.
|
||||
* Returns an array of {key, lines} pairs, preserving order and duplicates.
|
||||
*/
|
||||
function splitByTags(block: string): { key: string, lines: string[] }[] {
|
||||
const sections: { key: string, lines: string[] }[] = []
|
||||
const lines = block.split('\n')
|
||||
let currentKey = '_desc'
|
||||
let current: string[] = []
|
||||
|
||||
function flush() {
|
||||
if (current.length > 0) {
|
||||
sections.push({ key: currentKey, lines: [...current] })
|
||||
current = []
|
||||
}
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
const tagMatch = trimmed.match(/^@(param|return|example)\b/)
|
||||
if (tagMatch) {
|
||||
flush()
|
||||
currentKey = tagMatch[1]
|
||||
current.push(line)
|
||||
}
|
||||
else {
|
||||
current.push(line)
|
||||
}
|
||||
}
|
||||
flush()
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `@param {Type} .Name - description` lines from param sections.
|
||||
*/
|
||||
function parseParams(paramLines: string[]): PartialParam[] {
|
||||
const params: PartialParam[] = []
|
||||
let current: PartialParam | null = null
|
||||
|
||||
for (const line of paramLines) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed)
|
||||
continue
|
||||
|
||||
const paramMatch = trimmed.match(/^@param \{([^}]+)\} (\S+)(?:\s*- (.+))?$/)
|
||||
if (paramMatch) {
|
||||
current = {
|
||||
name: paramMatch[2],
|
||||
type: paramMatch[1],
|
||||
description: paramMatch[3] || '',
|
||||
children: [],
|
||||
}
|
||||
params.push(current)
|
||||
continue
|
||||
}
|
||||
|
||||
const childMatch = trimmed.match(/^-\s+(\w+): (.*)$/)
|
||||
if (childMatch && current) {
|
||||
current.children.push({
|
||||
name: childMatch[1],
|
||||
type: '',
|
||||
description: childMatch[2].trim(),
|
||||
children: [],
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (current && !trimmed.startsWith('@')) {
|
||||
current.description += (current.description ? ' ' : '') + trimmed
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `@return {Type} description` from return sections.
|
||||
*/
|
||||
function parseReturn(returnLines: string[]): string {
|
||||
for (const line of returnLines) {
|
||||
const match = line.trim().match(/@return\s+\{([^}]+)\}\s*(.*)/)
|
||||
if (match)
|
||||
return `\`${match[1]}\`${match[2] ? ` - ${match[2].trim()}` : ''}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `@example` blocks from example sections.
|
||||
*/
|
||||
/**
|
||||
* Strip common leading whitespace from an array of lines.
|
||||
*/
|
||||
function dedent(lines: string[]): string[] {
|
||||
const nonEmpty = lines.filter(l => l.trim() !== '')
|
||||
if (nonEmpty.length === 0)
|
||||
return lines
|
||||
const minIndent = Math.min(...nonEmpty.map(l => l.match(/^(\s*)/)![1].length))
|
||||
return lines.map(l => l.slice(minIndent))
|
||||
}
|
||||
|
||||
function parseExamples(exampleLines: string[]): string[] {
|
||||
const examples: string[] = []
|
||||
const current: string[] = []
|
||||
let firstLine = true
|
||||
|
||||
for (const line of exampleLines) {
|
||||
const trimmed = line.trim()
|
||||
if (firstLine) {
|
||||
firstLine = false
|
||||
const inline = trimmed.replace(/^@example\s*/, '')
|
||||
if (inline) {
|
||||
examples.push(inline)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
current.push(line)
|
||||
}
|
||||
|
||||
if (current.length > 0) {
|
||||
examples.push(dedent(current).join('\n').trim())
|
||||
}
|
||||
|
||||
return examples.filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract description text from the pre-tag section.
|
||||
* Preserves bullet lists (lines starting with `- `) and paragraph breaks.
|
||||
*/
|
||||
function parseDescription(descLines: string[]): string {
|
||||
const lines = descLines
|
||||
.map(l => l.trim())
|
||||
.filter(l => l !== '---' && !l.startsWith('- .'))
|
||||
|
||||
const result: string[] = []
|
||||
let paragraph: string[] = []
|
||||
|
||||
function flushParagraph() {
|
||||
if (paragraph.length > 0) {
|
||||
let text = paragraph.join(' ')
|
||||
if (text && !/^https?:\/\//i.test(text))
|
||||
text = text.charAt(0).toUpperCase() + text.slice(1)
|
||||
if (text && !/[.!?:]$/.test(text))
|
||||
text += '.'
|
||||
result.push(text)
|
||||
paragraph = []
|
||||
}
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === '') {
|
||||
flushParagraph()
|
||||
}
|
||||
else if (line.startsWith('- ')) {
|
||||
flushParagraph()
|
||||
const text = line.slice(2)
|
||||
const capitalized = /^https?:\/\//i.test(text) ? text : `${text.charAt(0).toUpperCase()}${text.slice(1)}`
|
||||
result.push(`- ${capitalized}`)
|
||||
}
|
||||
else {
|
||||
paragraph.push(line)
|
||||
}
|
||||
}
|
||||
flushParagraph()
|
||||
|
||||
// Join with double newlines, but consecutive list items use single newlines
|
||||
let output = ''
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (i > 0) {
|
||||
const prevIsList = result[i - 1].startsWith('- ')
|
||||
const currIsList = result[i].startsWith('- ')
|
||||
output += (prevIsList && currIsList) ? '\n' : '\n\n'
|
||||
}
|
||||
output += result[i]
|
||||
}
|
||||
|
||||
return output.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single partial HTML file into a PartialDoc.
|
||||
*/
|
||||
function parsePartialFile(filePath: string, relativePath: string): PartialDoc {
|
||||
const content = fs.readFileSync(filePath, 'utf-8')
|
||||
const comment = extractComment(content)
|
||||
|
||||
const doc: PartialDoc = {
|
||||
name: relativePath,
|
||||
path: relativePath,
|
||||
description: '',
|
||||
params: [],
|
||||
returns: '',
|
||||
examples: [],
|
||||
}
|
||||
|
||||
if (!comment)
|
||||
return doc
|
||||
|
||||
const sections = splitByTags(comment)
|
||||
const descLines = sections.filter(s => s.key === '_desc').flatMap(s => s.lines)
|
||||
const paramLines = sections.filter(s => s.key === 'param').flatMap(s => s.lines)
|
||||
const returnLines = sections.filter(s => s.key === 'return').flatMap(s => s.lines)
|
||||
const exampleSections = sections.filter(s => s.key === 'example')
|
||||
|
||||
doc.description = parseDescription(descLines)
|
||||
doc.params = parseParams(paramLines)
|
||||
doc.returns = parseReturn(returnLines)
|
||||
doc.examples = exampleSections.flatMap(s => parseExamples(s.lines))
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collect all .html files from a directory.
|
||||
*/
|
||||
function collectHtmlFiles(dir: string, baseDir: string): { filePath: string, relativePath: string }[] {
|
||||
const results: { filePath: string, relativePath: string }[] = []
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
const relativePath = path.relative(baseDir, fullPath)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...collectHtmlFiles(fullPath, baseDir))
|
||||
}
|
||||
else if (entry.name.endsWith('.html')) {
|
||||
results.push({ filePath: fullPath, relativePath })
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all Hugo partials from layouts/_partials/ and group by directory.
|
||||
*/
|
||||
export function parsePartials(partialsDir?: string): PartialGroup[] {
|
||||
const dir = partialsDir || fromRoot('layouts/_partials')
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
throw new Error(`Partials directory not found: ${dir}`)
|
||||
}
|
||||
|
||||
const files = collectHtmlFiles(dir, dir)
|
||||
const groupMap = new Map<string, PartialDoc[]>()
|
||||
|
||||
for (const { filePath, relativePath } of files) {
|
||||
const doc = parsePartialFile(filePath, relativePath)
|
||||
const dirName = path.dirname(relativePath)
|
||||
const group = dirName === '.' ? '(root)' : dirName
|
||||
|
||||
if (!groupMap.has(group)) {
|
||||
groupMap.set(group, [])
|
||||
}
|
||||
groupMap.get(group)!.push(doc)
|
||||
}
|
||||
|
||||
// Sort groups by name, with (root) last
|
||||
const groups: PartialGroup[] = []
|
||||
const sortedKeys = [...groupMap.keys()].sort((a, b) => {
|
||||
if (a === '(root)')
|
||||
return 1
|
||||
if (b === '(root)')
|
||||
return -1
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
|
||||
for (const key of sortedKeys) {
|
||||
const partials = groupMap.get(key)!.sort((a, b) => a.name.localeCompare(b.name))
|
||||
groups.push({
|
||||
name: key,
|
||||
description: GROUP_DESCRIPTIONS[key] || '',
|
||||
partials,
|
||||
})
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
function now(): string {
|
||||
const d = new Date()
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T00:00:00+08:00`
|
||||
}
|
||||
|
||||
export function renderConfig(body: string): string {
|
||||
const lines: string[] = []
|
||||
|
||||
lines.push('---')
|
||||
lines.push('title: Configuration Reference')
|
||||
lines.push('shortTitle: Configuration')
|
||||
lines.push(`date: ${now()}`)
|
||||
lines.push('description: Hugo configuration reference for the FixIt Hugo theme.')
|
||||
lines.push('collections:')
|
||||
lines.push(' - References')
|
||||
lines.push('---')
|
||||
lines.push('')
|
||||
lines.push('This page is auto-generated from `hugo.toml`. Do not edit manually.')
|
||||
lines.push('')
|
||||
lines.push('<!--more-->')
|
||||
lines.push('')
|
||||
lines.push(body)
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render standalone config docs with frontmatter (for CLI -o output).
|
||||
*/
|
||||
export function renderConfigStandalone(body: string): string {
|
||||
const lines: string[] = []
|
||||
|
||||
lines.push('---')
|
||||
lines.push('title: FixIt Theme Configuration')
|
||||
lines.push(`date: ${now()}`)
|
||||
lines.push('---')
|
||||
lines.push('')
|
||||
lines.push(body)
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { PartialDoc, PartialGroup } from '../parsers/partial-parser'
|
||||
|
||||
/**
|
||||
* Escape HTML tags in text to prevent them from being rendered as actual HTML.
|
||||
* Skips content inside backtick code spans.
|
||||
*/
|
||||
function escapeHtml(text: string): string {
|
||||
return text.replace(/`[^`]*`|<[^>]+>/g, (match) => {
|
||||
if (match.startsWith('`'))
|
||||
return match
|
||||
return match.replace(/</g, '<').replace(/>/g, '>')
|
||||
})
|
||||
}
|
||||
|
||||
function renderParamTable(params: PartialDoc['params']): string {
|
||||
if (params.length === 0)
|
||||
return ''
|
||||
|
||||
const lines: string[] = []
|
||||
lines.push('**Parameters:**')
|
||||
lines.push('')
|
||||
lines.push('| Name | Type | Description |')
|
||||
lines.push('|------|------|-------------|')
|
||||
|
||||
for (const param of params) {
|
||||
const name = `\`${param.name}\``
|
||||
const type = param.type ? `\`${param.type}\`` : ''
|
||||
const desc = escapeHtml(param.description || '')
|
||||
lines.push(`| ${name} | ${type} | ${desc} |`)
|
||||
|
||||
for (const child of param.children) {
|
||||
const childFullName = param.name === '.' ? `.${child.name}` : `${param.name}.${child.name}`
|
||||
const childName = `\`${childFullName}\``
|
||||
const childType = child.type ? `\`${child.type}\`` : ''
|
||||
const childDesc = escapeHtml(child.description || '')
|
||||
lines.push(`| ${childName} | ${childType} | ${childDesc} |`)
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function renderPartial(doc: PartialDoc): string {
|
||||
const lines: string[] = []
|
||||
|
||||
lines.push(`### ${doc.name}`)
|
||||
lines.push('')
|
||||
|
||||
if (!doc.description && doc.params.length === 0 && !doc.returns && doc.examples.length === 0) {
|
||||
lines.push('_No documentation._')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
if (doc.description) {
|
||||
lines.push(escapeHtml(doc.description))
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
const paramTable = renderParamTable(doc.params)
|
||||
if (paramTable) {
|
||||
lines.push(paramTable)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
if (doc.returns) {
|
||||
lines.push(`**Returns:** ${escapeHtml(doc.returns)}`)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
if (doc.examples.length > 0) {
|
||||
lines.push('**Example:**')
|
||||
lines.push('')
|
||||
for (const example of doc.examples) {
|
||||
lines.push('```go-template')
|
||||
lines.push((example))
|
||||
lines.push('```')
|
||||
lines.push('')
|
||||
}
|
||||
}
|
||||
|
||||
// Trim trailing blank lines
|
||||
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
||||
lines.pop()
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function renderGroup(group: PartialGroup): string {
|
||||
const lines: string[] = []
|
||||
|
||||
const count = group.partials.length
|
||||
const header = group.name === '(root)' ? '(root)' : `${group.name}/`
|
||||
lines.push(`## ${header}`)
|
||||
lines.push('')
|
||||
|
||||
if (group.description) {
|
||||
lines.push(`> ${group.description}`)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
lines.push(`> ${count} partial${count !== 1 ? 's' : ''}`)
|
||||
lines.push('')
|
||||
|
||||
for (const partial of group.partials) {
|
||||
lines.push(renderPartial(partial))
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Hugo partials documentation from parsed groups.
|
||||
* @param groups Parsed partial groups
|
||||
* @returns Markdown documentation body (no frontmatter)
|
||||
*/
|
||||
export function generatePartialsDocs(groups: PartialGroup[]): string {
|
||||
const lines: string[] = []
|
||||
|
||||
// Summary
|
||||
const totalCount = groups.reduce((sum, g) => sum + g.partials.length, 0)
|
||||
lines.push(`The FixIt theme provides **${totalCount}** Hugo partials across **${groups.length}** groups.`)
|
||||
lines.push('')
|
||||
|
||||
// TOC
|
||||
lines.push('## Groups')
|
||||
lines.push('')
|
||||
for (const group of groups) {
|
||||
const header = group.name === '(root)' ? '(root)' : `${group.name}/`
|
||||
const count = group.partials.length
|
||||
lines.push(`- [${header}](#${group.name.replace(/[()/]/g, '').toLowerCase()}) — ${count} partial${count !== 1 ? 's' : ''}`)
|
||||
}
|
||||
lines.push('')
|
||||
|
||||
// Groups
|
||||
for (const group of groups) {
|
||||
lines.push(renderGroup(group))
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext"],
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Generated
+792
-918
File diff suppressed because it is too large
Load Diff
@@ -7,5 +7,6 @@ packages:
|
||||
- packages/*
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
core-js: true
|
||||
esbuild: true
|
||||
simple-git-hooks: true
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"entryPoints": [
|
||||
"assets/js"
|
||||
],
|
||||
"entryPointStrategy": "expand",
|
||||
"tsconfig": "assets/tsconfig.json",
|
||||
"out": "../fixit-docs/static/references/javascript",
|
||||
"skipErrorChecking": true,
|
||||
"excludePrivate": true,
|
||||
"excludeInternal": true,
|
||||
"exclude": [
|
||||
"assets/js/types/index.ts",
|
||||
"assets/js/utils/index.ts",
|
||||
"**/main.ts",
|
||||
"**/head/**",
|
||||
"**/lib/**",
|
||||
"**/pages/**"
|
||||
],
|
||||
"name": "FixIt JavaScript API",
|
||||
"navigationLinks": {
|
||||
"FixIt Documentation": "https://fixit.lruihao.cn",
|
||||
"GitHub": "https://github.com/hugo-fixit/FixIt"
|
||||
},
|
||||
"categorizeByGroup": true,
|
||||
"readme": "none",
|
||||
"sourceLinkTemplate": "https://github.com/hugo-fixit/FixIt/blob/{gitRevision}/{path}#L{line}"
|
||||
}
|
||||
Reference in New Issue
Block a user