Craig Fletcher пре 2 недеља
родитељ
комит
ba228cb59c
5 измењених фајлова са 182 додато и 13 уклоњено
  1. 1 0
      README.md
  2. 8 5
      src/actions/generateTaxonomy/index.js
  3. 8 0
      src/actions/renderTemplate/README.md
  4. 61 4
      src/defaults.js
  5. 104 4
      src/lib.js

+ 1 - 0
README.md

@@ -161,6 +161,7 @@ jobConfig: {
 - `outputFileExtension`: New extension appended to output files (e.g. `.html`).
 - `outputFileName`: Base filename used when building a single output path for state tasks.
 - `buildFilePath`: When `true`, compute and pass `fileOutputPath` into the action for state tasks.
+- `buildIndexList`: When `true` (default), generate an `index.html` in the output dir listing all index pages (state expansion only).
 - `itemsPerPage`: Page size for state expansion pagination (defaults to `opts.itemsPerPage`).
 - `skipCache`: When `true`, disables cache usage for this job.
 - `deps`: Extra cache dependencies `{ paths?: string[], state?: string[] }` added to the job fingerprint.

+ 8 - 5
src/actions/generateTaxonomy/index.js

@@ -9,10 +9,10 @@ export async function generateTaxonomy({ config: actionConfig }) {
   }
   const allValues = actionConfig.inputs.reduce((values, curr) => {
     const items = curr[actionConfig.indexOn]
-    if (!Array.isArray(items)) {
-      return values
-    }
-    items.forEach(v => values.add(v))
+    const normalizedItems = Array.isArray(items)
+      ? items
+      : (items == null ? [] : [items])
+    normalizedItems.forEach(v => values.add(v))
     return values
   }, new Set())
   const orderBy = actionConfig.orderBy || "date"
@@ -26,7 +26,10 @@ export async function generateTaxonomy({ config: actionConfig }) {
         [currentGroup]: sortedInputs
           .filter(item => {
             const items = item?.[actionConfig.indexOn]
-            return Array.isArray(items) && items.includes(currentGroup)
+            const normalizedItems = Array.isArray(items)
+              ? items
+              : (items == null ? [] : [items])
+            return normalizedItems.includes(currentGroup)
           })
           .map(item => {
             const entry = actionConfig.properties

+ 8 - 0
src/actions/renderTemplate/README.md

@@ -10,6 +10,13 @@ Renders a Handlebars template with the current task state and optional output.
 - `writeOut` (boolean, optional): When `true`, writes minified HTML to `fileOutputPath`.
 - `filePath` (string, optional): Explicit template path override.
 - `fileOutputPath` (string, required when `writeOut` is true): Output HTML path.
+- `inputs` (array, computed): Expanded inputs for state jobs, or index list entries when `buildIndexList` is used.
+- `pagination` (object, computed): Pagination metadata for state expansion pages.
+- `stateKey` (string, computed): Key for the current state expansion item.
+- `indexPages` (array, computed): List of index pages when `buildIndexList` is enabled.
+- `isIndexList` (boolean, computed): `true` when rendering the index list page.
+- `indexOn` (string, computed): Lowercased index label for the current index page (e.g. `tag`).
+- `indexPage` (string, computed): Href to the index list page (e.g. `/blog/by-tag/`).
 
 ## Usage
 ```javascript
@@ -28,3 +35,4 @@ actionConfig: {
 ## Notes
 - All `actionConfig` properties are passed into the template context along with `meta`.
 - For state tasks, `fileOutputPath` is often provided via upstream `detail` or `buildFilePath`.
+- `indexPages` entries include `key`, `title`, `href`, `fileOutputPath`, `count`, and `totalPages`.

+ 61 - 4
src/defaults.js

@@ -75,22 +75,49 @@ export const tasks = [
       actionConfig: {},
     },
   ],
+  {
+    name: "blog-markdown",
+    action: renderMarkdownToHtml,
+    jobConfig: {
+      inputFiles: [{ pattern: "markdown/blog/*.md" }],
+      stripPaths: ["markdown/"],
+      outputFileExtension: ".html",
+    },
+    actionConfig: {},
+  },
   {
     name: "markdown",
     action: renderMarkdownToHtml,
     jobConfig: {
-      inputFiles: [{ pattern: "markdown/**/*.md" }],
+      inputFiles: [{ pattern: "markdown/*.md" }],
       stripPaths: ["markdown/"],
       outputFileExtension: ".html",
     },
     actionConfig: {},
   },
   [
+    {
+      name: "author-taxonomy",
+      action: generateTaxonomy,
+      jobConfig: {
+        stateSelectors: ["resources.blog-markdown"],
+        expand: false,
+        skipCache: true,
+      },
+      actionConfig: {
+        indexOn: "author",
+        orderBy: "date",
+        properties: [
+          "title", "href", "date", "author", "tags", "description",
+        ],
+        sortAscending: false,
+      },
+    },
     {
       name: "tag-taxonomy",
       action: generateTaxonomy,
       jobConfig: {
-        stateSelectors: ["resources.markdown"],
+        stateSelectors: ["resources.blog-markdown"],
         expand: false,
         skipCache: true,
       },
@@ -107,7 +134,7 @@ export const tasks = [
       name: "blog-latest",
       action: generateTaxonomy,
       jobConfig: {
-        stateSelectors: ["resources.markdown"],
+        stateSelectors: ["resources.blog-markdown"],
         expand: false,
         skipCache: true,
       },
@@ -145,7 +172,37 @@ export const tasks = [
       },
     },
     {
-      name: "render indexes",
+      name: "render blog pages",
+      action: renderTemplate,
+      jobConfig: {
+        stateSelectors: ["resources.blog-markdown"],
+      },
+      actionConfig: {
+        writeOut: true,
+        templateDirs: ["templates/", "~/.rhedyn/templates/"],
+        partialDirs: ["partials/", "~/.rhedyn/partials/"],
+        defaultTemplate: "article",
+      },
+    },
+    {
+      name: "render author indexes",
+      action: renderTemplate,
+      jobConfig: {
+        stateSelectors: ["resources.author-taxonomy.detail"],
+        outputFileExtension: ".html",
+        outputDir: "blog/by-author/",
+        buildFilePath: true,
+        itemsPerPage: 10,
+      },
+      actionConfig: {
+        writeOut: true,
+        template: "index",
+        templateDirs: ["templates/", "~/.rhedyn/templates/"],
+        partialDirs: ["partials/", "~/.rhedyn/partials/"],
+      },
+    },
+    {
+      name: "render tag indexes",
       action: renderTemplate,
       jobConfig: {
         stateSelectors: ["resources.tag-taxonomy.detail"],

+ 104 - 4
src/lib.js

@@ -7,6 +7,7 @@ import {
   removeCwd,
   replaceFileExtension,
   getValueAtPath,
+  getHref,
   expandTilde,
 } from "./util/index.js"
 import path from "path"
@@ -80,6 +81,19 @@ function buildPaginationMeta(page, totalPages, basePath) {
   }
 }
 
+function getIndexPropertyLabel(propertyPath) {
+  if (!propertyPath) return "Index"
+  let label = propertyPath
+  label = label.replace(/^resources\./, "")
+  label = label.replace(/\.detail$/, "")
+  label = label.replace(/\.items$/, "")
+  const parts = label.split(".")
+  label = parts[parts.length - 1] || label
+  label = label.replace(/-taxonomy$/, "")
+  label = label.replace(/[-_]+/g, " ")
+  return label.replace(/\b\w/g, char => char.toUpperCase())
+}
+
 export async function getConfig() {
   const args = process.argv.slice(2)
   const defaultPath = path.join(process.cwd(), "rhedyn.config.js")
@@ -259,6 +273,22 @@ async function expandStateTask(stateToExpand, task, jobConfig, actionConfig, met
   const stateToProcess = selectState(stateToExpand, meta)
   const pathsToStrip = getPathsToStrip(jobConfig)
   const itemsPerPage = jobConfig.itemsPerPage ?? meta.opts.itemsPerPage
+  const shouldSetIndexTitle = jobConfig.buildFilePath && actionConfig.writeOut
+  const shouldBuildIndexList =
+    jobConfig.buildIndexList !== false &&
+    shouldSetIndexTitle
+  const indexListHref = shouldBuildIndexList
+    ? getHref(
+      buildOutputPath(
+        meta.opts.outDir,
+        jobConfig.outputDir,
+        pathsToStrip,
+        "index",
+        jobConfig.outputFileExtension,
+      ),
+      meta,
+    )
+    : null
 
   // Build jobs, potentially with pagination
   const jobs = stateToProcess.flatMap((stateJob, index) => {
@@ -294,12 +324,21 @@ async function expandStateTask(stateToExpand, task, jobConfig, actionConfig, met
         ? buildPaginationMeta(page, totalPages, basePath.replace(meta.opts.outDir, "/"))
         : null
 
+      const propertyLabel = getIndexPropertyLabel(stateJob.property)
+      const indexOn = propertyLabel.toLowerCase()
+      const titleValue = stateJob.key != null ? String(stateJob.key) : String(index)
       const decorations = {
         ...(Array.isArray(stateJob.value)
           ? { inputs: pageItems }
           : { ...stateJob.value?.detail, inputs: pageItems }),
         ...(jobConfig.buildFilePath ? { fileOutputPath } : {}),
         ...(pagination ? { pagination } : {}),
+        ...(shouldSetIndexTitle && actionConfig.title == null
+          ? { title: `${propertyLabel}: ${titleValue}` }
+          : {}),
+        ...(indexListHref
+          ? { indexOn, indexPage: indexListHref }
+          : {}),
       }
 
       return {
@@ -312,8 +351,7 @@ async function expandStateTask(stateToExpand, task, jobConfig, actionConfig, met
     })
   })
 
-  return await Promise.all(
-    jobs.map(async ({ stateJob, page, decorations }) => {
+  const jobPromises = jobs.map(async ({ stateJob, page, decorations }) => {
       const fileOutputDir = decorations.fileOutputPath
         ? path.dirname(decorations.fileOutputPath)
         : undefined
@@ -332,8 +370,66 @@ async function expandStateTask(stateToExpand, task, jobConfig, actionConfig, met
         actionConfig: jobActionConfig,
         jobId: `${task.name} @ ${stateJob.property}:${stateJob.index}${pageInfo}`,
       })
-    }),
-  )
+    })
+
+  if (shouldBuildIndexList) {
+    const indexPropertyLabel = getIndexPropertyLabel(stateToProcess[0]?.property)
+    const indexPages = stateToProcess.map((stateJob, index) => {
+      const key = stateJob.key ?? String(index)
+      const items = Array.isArray(stateJob.value)
+        ? stateJob.value
+        : stateJob.value?.detail || []
+      const totalPages = Array.isArray(items) && itemsPerPage > 0
+        ? Math.ceil(items.length / itemsPerPage)
+        : 1
+      const fileOutputPath = buildOutputPath(
+        meta.opts.outDir,
+        jobConfig.outputDir,
+        pathsToStrip,
+        key,
+        jobConfig.outputFileExtension,
+      )
+      return {
+        key,
+        title: String(key),
+        href: getHref(fileOutputPath, meta),
+        fileOutputPath,
+        count: Array.isArray(items) ? items.length : 0,
+        totalPages,
+      }
+    })
+
+    const indexFileOutputPath = buildOutputPath(
+      meta.opts.outDir,
+      jobConfig.outputDir,
+      pathsToStrip,
+      "index",
+      jobConfig.outputFileExtension,
+    )
+    const indexJobActionConfig = {
+      ...actionConfig,
+      inputs: indexPages,
+      indexPages,
+      isIndexList: true,
+      fileOutputPath: indexFileOutputPath,
+      fileOutputDir: path.dirname(indexFileOutputPath),
+      outputFileExtension: jobConfig.outputFileExtension,
+    }
+    if (indexJobActionConfig.title == null) {
+      indexJobActionConfig.title = `By: ${indexPropertyLabel}`
+    }
+    jobPromises.push(
+      runTask({
+        meta,
+        action: task.action,
+        jobConfig,
+        actionConfig: indexJobActionConfig,
+        jobId: `${task.name} @ index-list`,
+      }),
+    )
+  }
+
+  return await Promise.all(jobPromises)
 }
 
 export async function expandAndRunTask(meta, task) {
@@ -353,6 +449,7 @@ export async function expandAndRunTask(meta, task) {
         ...actionConfig,
         inputs,
         outputFileExtension: jobConfig.outputFileExtension,
+        ...(actionConfig.isIndexList == null ? { isIndexList: true } : {}),
       }
       const taskResult = await runTask({
         meta,
@@ -402,6 +499,7 @@ export async function expandAndRunTask(meta, task) {
           inputs,
           ...(fileOutputDir ? { fileOutputDir } : {}),
           outputFileExtension: jobConfig.outputFileExtension,
+          ...(actionConfig.isIndexList == null ? { isIndexList: true } : {}),
         }
         const jobId = jobConfig.jobId || task.name
         const taskResult = await runTask({
@@ -430,6 +528,7 @@ export async function expandAndRunTask(meta, task) {
           inputs,
           ...(fileOutputDir ? { fileOutputDir } : {}),
           outputFileExtension: jobConfig.outputFileExtension,
+          ...(actionConfig.isIndexList == null ? { isIndexList: true } : {}),
         }
         const jobId = jobConfig.jobId || task.name
         const taskResult = await runTask({
@@ -476,6 +575,7 @@ export async function expandAndRunTask(meta, task) {
             inputs: pageItems,
             ...(fileOutputDir ? { fileOutputDir } : {}),
             outputFileExtension: jobConfig.outputFileExtension,
+            ...(actionConfig.isIndexList == null ? { isIndexList: true } : {}),
           }
           const pageInfo = page > 1 ? `:page-${page}` : ""
           return runTask({