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**
|
- Globally consistent **design language**
|
||||||
- **Pagination** supported
|
- **Pagination** supported
|
||||||
- Easy-to-use and self-expanding **table of contents**
|
- Easy-to-use and self-expanding **table of contents**
|
||||||
- **Multilanguage** supported and i18n ready
|
- **Multilanguage** supported and I18n ready
|
||||||
- Beautiful **CSS animation**
|
- Beautiful **CSS animation**
|
||||||
|
|
||||||
### Social and Comment Systems
|
### Social and Comment Systems
|
||||||
@@ -168,7 +168,7 @@ Click the following links to generate a new repository with template:
|
|||||||
|
|
||||||
</details>
|
</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).
|
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>
|
</details>
|
||||||
|
|
||||||
|
## Browsers Support
|
||||||
|
|
||||||
|
The FixIt theme supports the last two versions of all major browsers.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
## Theme Components
|
## Theme Components
|
||||||
|
|
||||||
To balance **simplicity** and **extensibility**, FixIt theme provides extra [Hugo theme components](https://fixit.lruihao.cn/ecosystem/#-components) for customization.
|
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>
|
</details>
|
||||||
|
|
||||||
|
## 浏览器支持
|
||||||
|
|
||||||
|
FixIt 主题支持所有主流浏览器的最近两个版本。
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
## 主题组件
|
## 主题组件
|
||||||
|
|
||||||
为了平衡 FixIt 主题的 **简洁性** 和 **可扩展性**,我们额外开发了一系列 [Hugo 主题组件](https://fixit.lruihao.cn/zh-cn/ecosystem/#-组件) 供用户选择。
|
为了平衡 FixIt 主题的 **简洁性** 和 **可扩展性**,我们额外开发了一系列 [Hugo 主题组件](https://fixit.lruihao.cn/zh-cn/ecosystem/#-组件) 供用户选择。
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export type FixItDocumentEventMap = {
|
|||||||
[K in keyof FixItEventMap]: CustomEvent<FixItEventMap[K]>
|
[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)
|
? (() => void) | ((event: CustomEvent<void>) => void)
|
||||||
: (event: CustomEvent<T>) => void
|
: (event: CustomEvent<T>) => void
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,14 @@
|
|||||||
@forward "hugo:vars";
|
@forward "hugo:vars";
|
||||||
@forward "hugo:vars/internal";
|
@forward "hugo:vars/internal";
|
||||||
|
|
||||||
|
/// CSS variable prefix for all custom properties.
|
||||||
|
/// @type String
|
||||||
$prefix: fi- !default;
|
$prefix: fi- !default;
|
||||||
|
|
||||||
|
/// Root prefix for CSS custom properties (derived from $prefix).
|
||||||
|
/// @type String
|
||||||
$rootPrefix: --#{$prefix} !default;
|
$rootPrefix: --#{$prefix} !default;
|
||||||
|
|
||||||
|
/// Default header height.
|
||||||
|
/// @type Length
|
||||||
$header-height: 3.5rem !default;
|
$header-height: 3.5rem !default;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Color map of admonitions
|
/// Color map of admonitions.
|
||||||
|
/// @type Map
|
||||||
$admonition-color-map: (
|
$admonition-color-map: (
|
||||||
note: (
|
note: (
|
||||||
color: #448aff,
|
color: #448aff,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Color map of basic alerts
|
/// Color map of GitHub-style alerts (light theme).
|
||||||
|
/// @type Map
|
||||||
$alert-color-map: (
|
$alert-color-map: (
|
||||||
note: (
|
note: (
|
||||||
border-color: #0969da,
|
border-color: #0969da,
|
||||||
@@ -22,6 +23,8 @@ $alert-color-map: (
|
|||||||
),
|
),
|
||||||
) !default;
|
) !default;
|
||||||
|
|
||||||
|
/// Color map of GitHub-style alerts (dark theme).
|
||||||
|
/// @type Map
|
||||||
$alert-color-map-dark: (
|
$alert-color-map-dark: (
|
||||||
note: (
|
note: (
|
||||||
border-color: #316dca,
|
border-color: #316dca,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// Map of code identifiers/languages to Chroma supported languages.
|
/// Map of code identifiers/languages to Chroma supported languages.
|
||||||
/// This file is auto-generated by `pnpm gen:lexers`, do not edit manually.
|
/// 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: (
|
$chroma-lexers: (
|
||||||
"🔥": "Mojo",
|
"🔥": "Mojo",
|
||||||
"1s": "OnesEnterprise",
|
"1s": "OnesEnterprise",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Theme colors map
|
/// Theme semantic color tokens.
|
||||||
|
/// @type Map
|
||||||
$theme-colors: (
|
$theme-colors: (
|
||||||
'primary': #1677ff,
|
'primary': #1677ff,
|
||||||
'secondary': #8b949e,
|
'secondary': #8b949e,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
@use "core/functions" as *;
|
@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) {
|
@mixin overflow-wrap($value) {
|
||||||
word-wrap: $value;
|
word-wrap: $value;
|
||||||
overflow-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) {
|
@mixin focus-visible-ring($size: 1.3em, $offset: 0.2rem) {
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@use "border-radius" as *;
|
@use "border-radius" as *;
|
||||||
@use "compatibility" as *;
|
@use "compatibility" as *;
|
||||||
|
|
||||||
|
/// Shared inline code styles using theme CSS variables.
|
||||||
@mixin inline-code() {
|
@mixin inline-code() {
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@use "core/functions" as *;
|
@use "core/functions" as *;
|
||||||
@use "z-index" as *;
|
@use "z-index" as *;
|
||||||
|
|
||||||
// Loading Mixin for before/after pseudo element
|
/// Loading pseudo-element styles for async content.
|
||||||
@mixin loading {
|
@mixin loading {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
@use "sass:map";
|
@use "sass:map";
|
||||||
@use "sass:list";
|
@use "sass:list";
|
||||||
|
|
||||||
|
/// Responsive breakpoints map for the `media` mixin.
|
||||||
|
/// @type Map
|
||||||
$breakpoints: (
|
$breakpoints: (
|
||||||
'xs': null,
|
'xs': null,
|
||||||
'sm': 680px,
|
'sm': 680px,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
@use "theme-vars" as *;
|
@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) {
|
@mixin scrollbar-width($width: thin, $widthLegacy: 12px) {
|
||||||
@include set-fi-vars((
|
@include set-fi-vars((
|
||||||
scrollbar-width: $width,
|
scrollbar-width: $width,
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
@use "variables" as *;
|
@use "variables" as *;
|
||||||
|
|
||||||
// Shared defaults for theme-switch icon motion.
|
/// Duration of the theme-switch icon animation.
|
||||||
// Keep them overridable so other entries can reuse the same mixin with different pacing.
|
/// @type Time
|
||||||
$theme-switch-animation-duration: 0.5s !default;
|
$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;
|
$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.
|
/// Bind a theme mode selector to an icon glyph and its matching animation.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@use "sass:map";
|
@use "sass:map";
|
||||||
|
|
||||||
|
/// Z-index scale for consistent layering across the theme.
|
||||||
|
/// @type Map
|
||||||
$z-indexes: (
|
$z-indexes: (
|
||||||
"hide": -1,
|
"hide": -1,
|
||||||
"auto": auto,
|
"auto": auto,
|
||||||
|
|||||||
@@ -676,7 +676,7 @@ wrapper = true
|
|||||||
cdn = "https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs"
|
cdn = "https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs"
|
||||||
# ZenUML ESM module CDN source.
|
# ZenUML ESM module CDN source.
|
||||||
# You can set it to a CDN source to enable ZenUML support.
|
# 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 = ""
|
zenuml = ""
|
||||||
# For values, See: [Available Themes](https://mermaid.js.org/config/theming.html#available-themes).
|
# For values, See: [Available Themes](https://mermaid.js.org/config/theming.html#available-themes).
|
||||||
themes = [
|
themes = [
|
||||||
@@ -692,7 +692,7 @@ look = "handDrawn"
|
|||||||
# Specifies the font to be used in the rendered diagrams.
|
# Specifies the font to be used in the rendered diagrams.
|
||||||
font_family = ""
|
font_family = ""
|
||||||
# Layout loaders from ESM module CDN sources (e.g., ELK layout engine).
|
# 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"].
|
# This will enable additional layout options: ["elk", "elk.layered", "elk.stress", "elk.force", "elk.mrtree"].
|
||||||
layout_loaders = []
|
layout_loaders = []
|
||||||
# Default layout algorithm for rendering diagrams.
|
# Default layout algorithm for rendering diagrams.
|
||||||
@@ -859,9 +859,9 @@ user_desc = ""
|
|||||||
# DOM container to be blacked out, e.g. [".aplayer"]
|
# DOM container to be blacked out, e.g. [".aplayer"]
|
||||||
black_dom = []
|
black_dom = []
|
||||||
# only for iframe mode
|
# only for iframe mode
|
||||||
# Example: "375px".
|
# Example: "375px"
|
||||||
frame_width = ""
|
frame_width = ""
|
||||||
# Example: "600px".
|
# Example: "600px"
|
||||||
frame_height = ""
|
frame_height = ""
|
||||||
# Only for magic mode.
|
# Only for magic mode.
|
||||||
user_icon = ""
|
user_icon = ""
|
||||||
@@ -1163,21 +1163,21 @@ lazy_load = true
|
|||||||
[params.library]
|
[params.library]
|
||||||
|
|
||||||
# Load some CSS from local assets.
|
# Load some CSS from local assets.
|
||||||
# Example: someCSS = "css/some.css".
|
# Example: `someCSS = "css/some.css"`
|
||||||
# Load some CSS from remote CDN.
|
# 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]
|
[params.library.css]
|
||||||
|
|
||||||
# Load some JS from local assets.
|
# Load some JS from local assets.
|
||||||
# Example: someJS = "js/some.js".
|
# Example: `someJS = "js/some.js"`
|
||||||
# Load some JS from remote CDN.
|
# 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]
|
[params.library.js]
|
||||||
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
# Public Git repository information only then enableGitInfo is true.
|
# Public Git repository information only then enableGitInfo is true.
|
||||||
[params.git_info]
|
[params.git_info]
|
||||||
# Example: "https://github.com/hugo-fixit/docs".
|
# Example: `"https://github.com/hugo-fixit/docs"`
|
||||||
repo = ""
|
repo = ""
|
||||||
branch = "main"
|
branch = "main"
|
||||||
# The content directory path relative to the root of the repository.
|
# The content directory path relative to the root of the repository.
|
||||||
@@ -1373,7 +1373,7 @@ enable = false
|
|||||||
animate = true
|
animate = true
|
||||||
icon = "fa-solid fa-heartbeat"
|
icon = "fa-solid fa-heartbeat"
|
||||||
pre = ""
|
pre = ""
|
||||||
# Example: "2021-12-18T16:15:22+08:00".
|
# Example: "2021-12-18T16:15:22+08:00"
|
||||||
value = ""
|
value = ""
|
||||||
|
|
||||||
# Footer lines order.
|
# Footer lines order.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
Loads the main stylesheet (every page) and conditionally loads
|
Loads the main stylesheet (every page) and conditionally loads
|
||||||
page-specific CSS based on .Kind, .Layout, and .RelPermalink.
|
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" . -}}
|
{{- $scssVars := partialCached "function/scss-vars.html" . -}}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
@example
|
@example
|
||||||
// Practical usage in templates
|
// 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 -}}
|
{{- $input := . | default dict -}}
|
||||||
{{- $output := dict -}}
|
{{- $output := dict -}}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
@param {String} . - The snake_case string
|
@param {String} . - The snake_case string
|
||||||
@return {String} The camelCase 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 . "_" -}}
|
{{- $parts := split . "_" -}}
|
||||||
{{- $result := index $parts 0 -}}
|
{{- $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 := . -}}
|
||||||
{{- $content = replace $content "#" "%23" -}}
|
{{- $content = replace $content "#" "%23" -}}
|
||||||
|
|||||||
@@ -21,8 +21,9 @@
|
|||||||
|
|
||||||
@example
|
@example
|
||||||
{{- $cover := dict "Page" . | partial "function/get-cover.html" -}}
|
{{- $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" -}}
|
{{- $cover := dict "Page" . "Preview" true | partial "function/get-cover.html" -}}
|
||||||
{{- dict "Src" $cover.URL "Resources" .Resources "Matches" $cover.Matches | partial "plugin/image.html" -}}
|
{{- dict "Src" $cover.URL "Resources" .Resources "Matches" $cover.Matches | partial "plugin/image.html" -}}
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
{{- /*
|
{{- /*
|
||||||
Example:
|
Validate if the given path has a valid suffix.
|
||||||
{{- $suffixList := slice ".jpeg" ".jpg" ".png" ".gif" ".bmp" ".tif" ".tiff" ".webp" ".avif" ".svg" -}}
|
@param Path string The path to validate.
|
||||||
{{- $suffixValid := (dict "Path" .Path "Suffixes" $suffixList | partial "function/suffix-validation.html") -}}
|
@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 -}}
|
{{- $url := urls.Parse .Path -}}
|
||||||
{{- $path := path.Clean ((printf "%v%v" $url.Host $url.Path) | lower) -}}
|
{{- $path := path.Clean ((printf "%v%v" $url.Host $url.Path) | lower) -}}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{{- /*
|
{{- /*
|
||||||
Icon renderer for Font Awesome classes and SVG sources.
|
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.
|
- Resolves local SVG resources or external SVG URL fallback.
|
||||||
- Supports Simple Icons shorthand by icon name.
|
- 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} [.Src] - SVG source path or URL
|
||||||
@param {String} [.Simpleicons] - Simple-icons icon name
|
@param {String} [.Simpleicons] - Simple-icons icon name
|
||||||
@param {String} [.Prefix] - Custom prefix for simple-icons source path
|
@param {String} [.Prefix] - Custom prefix for simple-icons source path
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{- /*
|
{{- /*
|
||||||
Social profile link helper.
|
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.
|
- Delegates final rendering to plugin/link.html with rel=me.
|
||||||
@param {String} [.Url] - Explicit target URL
|
@param {String} [.Url] - Explicit target URL
|
||||||
@param {String} [.Template] - URL template containing one %%v placeholder
|
@param {String} [.Template] - URL template containing one %%v placeholder
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{- /*
|
{{- /*
|
||||||
Stylesheet tag renderer.
|
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.
|
- 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 {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)
|
@param {resource.Resource} [.Resource] - Pre-built resource (skips build pipeline)
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
"version": "pnpm -F versioning start prod",
|
"version": "pnpm -F versioning start prod",
|
||||||
"changelog": "auto-changelog-plus --starting-date 2021-12-18",
|
"changelog": "auto-changelog-plus --starting-date 2021-12-18",
|
||||||
"gen:lexers": "pnpm -F chroma-lexers start",
|
"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": "unocss",
|
||||||
"unocss:watch": "unocss --watch",
|
"unocss:watch": "unocss --watch",
|
||||||
"prepare": "simple-git-hooks"
|
"prepare": "simple-git-hooks"
|
||||||
@@ -61,6 +64,7 @@
|
|||||||
"serve": "^14.2.6",
|
"serve": "^14.2.6",
|
||||||
"simple-git-hooks": "^2.13.1",
|
"simple-git-hooks": "^2.13.1",
|
||||||
"tsx": "^4.22.4",
|
"tsx": "^4.22.4",
|
||||||
|
"typedoc": "^0.28.20",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"unocss": "^66.7.4"
|
"unocss": "^66.7.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function generateScss(lexers: LexerEntry[]): void {
|
|||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
'/// Map of code identifiers/languages to Chroma supported languages.',
|
'/// Map of code identifiers/languages to Chroma supported languages.',
|
||||||
'/// This file is auto-generated by `pnpm gen:lexers`, do not edit manually.',
|
'/// 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: (',
|
'$chroma-lexers: (',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Then in your `package.json`:
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "hugo --gc --minify --logLevel info",
|
"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/*
|
- packages/*
|
||||||
allowBuilds:
|
allowBuilds:
|
||||||
'@parcel/watcher': true
|
'@parcel/watcher': true
|
||||||
|
core-js: true
|
||||||
esbuild: true
|
esbuild: true
|
||||||
simple-git-hooks: 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