Craig Fletcher 2 týždňov pred
rodič
commit
9195f6c557
3 zmenil súbory, kde vykonal 257 pridanie a 42 odobranie
  1. 6 0
      src/defaults.js
  2. 198 40
      src/lib.js
  3. 53 2
      src/processors.js

+ 6 - 0
src/defaults.js

@@ -101,6 +101,7 @@ export const tasks = [
       processor: renderTemplate,
       writeOut: true,
       templateDirs: ["templates/", "~/.rhedyn/templates/"],
+      partialDirs: ["partials/", "~/.rhedyn/partials/"],
       defaultTemplate: "page",
     },
     {
@@ -110,9 +111,11 @@ export const tasks = [
       writeOut: true,
       template: "index",
       templateDirs: ["templates/", "~/.rhedyn/templates/"],
+      partialDirs: ["partials/", "~/.rhedyn/partials/"],
       outputFileExtension: ".html",
       outputDir: "by-tag/",
       buildFilePath: true,
+      itemsPerPage: 10,
     },
     {
       name: "render blog home",
@@ -121,12 +124,14 @@ export const tasks = [
       writeOut: true,
       template: "index",
       templateDirs: ["templates/", "~/.rhedyn/templates/"],
+      partialDirs: ["partials/", "~/.rhedyn/partials/"],
       outputFileExtension: ".html",
       outputDir: "blog/",
       expand: false,
       outputFileName: "index",
       title: "Blog",
       buildFilePath: true,
+      itemsPerPage: 10,
     },
   ],
 ]
@@ -142,6 +147,7 @@ export const opts = {
   ignoreExisting: false,
   logLevel: "info",
   includeStateValues: true,
+  itemsPerPage: 25,
   site: {
     name: "Website generated by Rhedyn",
     shortName: "Rhedyn test site",

+ 198 - 40
src/lib.js

@@ -28,6 +28,58 @@ function getPathsToStrip(config) {
   return (config.stripPaths || []).map(p => expandTilde(p))
 }
 
+function paginateItems(items, itemsPerPage) {
+  if (!itemsPerPage || itemsPerPage <= 0 || !Array.isArray(items)) {
+    return [{ items, page: 1, totalPages: 1 }]
+  }
+  const totalPages = Math.ceil(items.length / itemsPerPage)
+  if (totalPages <= 1) {
+    return [{ items, page: 1, totalPages: 1 }]
+  }
+  const pages = []
+  for (let page = 1; page <= totalPages; page++) {
+    const start = (page - 1) * itemsPerPage
+    const end = start + itemsPerPage
+    pages.push({
+      items: items.slice(start, end),
+      page,
+      totalPages,
+    })
+  }
+  return pages
+}
+
+function buildPaginationMeta(page, totalPages, basePath) {
+  const ext = path.extname(basePath)
+  const base = basePath.slice(0, -ext.length)
+
+  const getPagePath = (pageNum) => {
+    if (pageNum === 1) return base.endsWith("/") ? base : `${base}/`
+    return `${base}/${pageNum}`
+  }
+
+  const pages = []
+  for (let i = 1; i <= totalPages; i++) {
+    pages.push({
+      pageNum: i,
+      path: getPagePath(i),
+      isCurrent: i === page,
+    })
+  }
+
+  return {
+    currentPage: page,
+    totalPages,
+    hasNextPage: page < totalPages,
+    hasPrevPage: page > 1,
+    nextPagePath: page < totalPages ? getPagePath(page + 1) : null,
+    prevPagePath: page > 1 ? getPagePath(page - 1) : null,
+    firstPagePath: getPagePath(1),
+    lastPagePath: getPagePath(totalPages),
+    pages,
+  }
+}
+
 export async function getConfig() {
   const args = process.argv.slice(2)
   const defaultPath = path.join(process.cwd(), "rhedyn.config.js")
@@ -201,34 +253,72 @@ function selectState(stateToSelect, meta) {
 async function expandStateTask(stateToExpand, config, meta) {
   const stateToProcess = selectState(stateToExpand, meta)
   const pathsToStrip = getPathsToStrip(config)
-  return await Promise.all(
-    stateToProcess.map(async (stateJob, index) => {
+  const itemsPerPage = config.itemsPerPage ?? meta.opts.itemsPerPage
+
+  // Build jobs, potentially with pagination
+  const jobs = stateToProcess.flatMap((stateJob, index) => {
+    const items = Array.isArray(stateJob.value)
+      ? stateJob.value
+      : stateJob.value?.detail || []
+
+    // Only paginate if items is an array and itemsPerPage is set
+    const shouldPaginate = Array.isArray(items) && itemsPerPage > 0
+    const pages = shouldPaginate
+      ? paginateItems(items, itemsPerPage)
+      : [{ items, page: 1, totalPages: 1 }]
+
+    return pages.map(({ items: pageItems, page, totalPages }) => {
+      const basePath = buildOutputPath(
+        meta.opts.outDir,
+        config.outputDir,
+        pathsToStrip,
+        stateJob.key || String(index),
+        config.outputFileExtension,
+      )
+
+      // For page 2+, modify the output path
+      const fileOutputPath = page === 1
+        ? basePath
+        : (() => {
+          const ext = path.extname(basePath)
+          const base = basePath.slice(0, -ext.length)
+          return `${base}/${page}${ext}`
+        })()
+
+      const pagination = totalPages > 1
+        ? buildPaginationMeta(page, totalPages, basePath.replace(meta.opts.outDir, "/"))
+        : null
+
       const decorations = {
         ...(Array.isArray(stateJob.value)
-          ? { inputs: stateJob.value }
-          : stateJob.value?.detail || {}),
-
-        ...(config.buildFilePath
-          ? {
-            fileOutputPath: buildOutputPath(
-              meta.opts.outDir,
-              config.outputDir,
-              pathsToStrip,
-              stateJob.key || String(index),
-              config.outputFileExtension,
-            ),
-          }
-          : {}),
+          ? { inputs: pageItems }
+          : { ...stateJob.value?.detail, inputs: pageItems }),
+        ...(config.buildFilePath ? { fileOutputPath } : {}),
+        ...(pagination ? { pagination } : {}),
+      }
+
+      return {
+        stateJob,
+        index,
+        page,
+        decorations,
+        fileOutputPath,
       }
+    })
+  })
+
+  return await Promise.all(
+    jobs.map(async ({ stateJob, page, decorations }) => {
       const jobConfig = {
         ...config,
         ...decorations,
         stateKey: stateJob.key,
       }
+      const pageInfo = page > 1 ? `:page-${page}` : ""
       return runTask({
         meta,
         config: jobConfig,
-        jobId: `${config.name} @ ${stateJob.property}:${stateJob.index}`,
+        jobId: `${config.name} @ ${stateJob.property}:${stateJob.index}${pageInfo}`,
       })
     }),
   )
@@ -259,31 +349,99 @@ export async function expandAndRunTask(meta, config) {
       const inputs = selectState(config.stateSelectors, meta).map(stateItem => {
         return stateItem.value?.detail ?? stateItem.value
       })
-      const decorations = {
-        ...(config.buildFilePath
-          ? {
-            fileOutputPath: buildOutputPath(
-              meta.opts.outDir,
-              config.outputDir,
-              pathsToStrip,
-              config.outputFileName || config.name,
-              config.outputFileExtension,
-            ),
-          }
-          : {}),
+      const itemsPerPage = config.itemsPerPage
+      const shouldPaginate =
+        Array.isArray(inputs) && itemsPerPage != null && itemsPerPage > 0
+      const basePath =
+        (config.buildFilePath || shouldPaginate)
+          ? buildOutputPath(
+            meta.opts.outDir,
+            config.outputDir,
+            pathsToStrip,
+            config.outputFileName || config.name,
+            config.outputFileExtension,
+          )
+          : null
+
+      if (!shouldPaginate) {
+        const decorations = {
+          ...(config.buildFilePath
+            ? { fileOutputPath: basePath }
+            : {}),
+        }
+        const jobConfig = {
+          ...config,
+          ...decorations,
+          inputs,
+        }
+        const jobId = config.jobId || config.name
+        const taskResult = await runTask({
+          meta,
+          config: { ...jobConfig },
+          jobId,
+        })
+        return [taskResult]
       }
-      const jobConfig = {
-        ...config,
-        ...decorations,
-        inputs,
+
+      const pages = paginateItems(inputs, itemsPerPage)
+      if (pages.length <= 1) {
+        const decorations = {
+          ...(config.buildFilePath
+            ? { fileOutputPath: basePath }
+            : {}),
+        }
+        const jobConfig = {
+          ...config,
+          ...decorations,
+          inputs,
+        }
+        const jobId = config.jobId || config.name
+        const taskResult = await runTask({
+          meta,
+          config: { ...jobConfig },
+          jobId,
+        })
+        return [taskResult]
       }
-      const jobId = config.jobId || config.name
-      const taskResult = await runTask({
-        meta,
-        config: { ...jobConfig },
-        jobId,
-      })
-      return [taskResult]
+
+      const basePathForPagination = basePath
+        ? basePath.replace(meta.opts.outDir, "/")
+        : null
+      const baseJobId = config.jobId || config.name
+      return await Promise.all(
+        pages.map(({ items: pageItems, page, totalPages }) => {
+          const fileOutputPath = basePath
+            ? (page === 1
+              ? basePath
+              : (() => {
+                const ext = path.extname(basePath)
+                const base = basePath.slice(0, -ext.length)
+                return `${base}/${page}${ext}`
+              })())
+            : undefined
+          const pagination =
+            totalPages > 1 && basePathForPagination
+              ? buildPaginationMeta(page, totalPages, basePathForPagination)
+              : null
+          const decorations = {
+            ...(config.buildFilePath && fileOutputPath
+              ? { fileOutputPath }
+              : {}),
+            ...(pagination ? { pagination } : {}),
+          }
+          const jobConfig = {
+            ...config,
+            ...decorations,
+            inputs: pageItems,
+          }
+          const pageInfo = page > 1 ? `:page-${page}` : ""
+          return runTask({
+            meta,
+            config: { ...jobConfig },
+            jobId: `${baseJobId}${pageInfo}`,
+          })
+        }),
+      )
     }
 
     return expandStateTask(config.stateSelectors, config, meta)

+ 53 - 2
src/processors.js

@@ -1,6 +1,7 @@
 import * as sass from "sass"
 import {
   escapeHtmlAttr,
+  expandTilde,
   firstFound,
   generateRandomId,
   getCleanPath,
@@ -8,6 +9,7 @@ import {
   slugifyString,
   writeFile,
 } from "./util/index.js"
+import { glob } from "glob"
 import fs from "fs/promises"
 import handlebars from "handlebars"
 import { marked } from "marked"
@@ -21,9 +23,55 @@ import favicons from "favicons"
 import _ from "lodash-es"
 
 const templateCache = new Map()
+const partialCache = new Map()
+const registeredPartials = new Set()
 
 export function clearTemplateCache() {
   templateCache.clear()
+  partialCache.clear()
+  registeredPartials.clear()
+}
+
+async function loadPartials(partialDirs) {
+  if (!partialDirs || partialDirs.length === 0) {
+    return []
+  }
+
+  const loadedPaths = []
+
+  for (const dir of partialDirs) {
+    const expandedDir = expandTilde(dir)
+    const pattern = path.join(expandedDir, "*.hbs")
+
+    let files
+    try {
+      files = await glob(pattern)
+    } catch {
+      continue
+    }
+
+    for (const filePath of files) {
+      const fileName = path.basename(filePath, ".hbs")
+      // Remove leading underscore if present (e.g., _pagination -> pagination)
+      const partialName = fileName.startsWith("_") ? fileName.slice(1) : fileName
+
+      if (registeredPartials.has(partialName)) {
+        continue
+      }
+
+      if (!partialCache.has(filePath)) {
+        const content = await fs.readFile(filePath, "utf8")
+        partialCache.set(filePath, { name: partialName, content })
+      }
+
+      const partial = partialCache.get(filePath)
+      handlebars.registerPartial(partial.name, partial.content)
+      registeredPartials.add(partialName)
+      loadedPaths.push(filePath)
+    }
+  }
+
+  return loadedPaths
 }
 
 function createMarkdownRenderer(meta) {
@@ -86,6 +134,9 @@ export async function renderTemplate({ config, meta }) {
   const fileOutputPath = config.fileOutputPath
   const href = getHref(fileOutputPath, meta)
 
+  // Load partials from configured directories
+  const partialPaths = await loadPartials(config.partialDirs)
+
   const template = await getTemplate(templatePath)
   const html = template.renderer({
     ...meta,
@@ -106,7 +157,7 @@ export async function renderTemplate({ config, meta }) {
     return {
       detail: { html },
       deps: {
-        paths: [template.path],
+        paths: [template.path, ...partialPaths],
       },
       paths: [fileOutputPath],
       ref: slugifyString(href),
@@ -116,7 +167,7 @@ export async function renderTemplate({ config, meta }) {
   return {
     detail: { html },
     deps: {
-      paths: [template.path],
+      paths: [template.path, ...partialPaths],
     },
     ref: slugifyString(href),
   }