@mdfriday/foundry
Version:
The core engine of MDFriday. Convert Markdown and shortcodes into fully themed static sites – Hugo-style, powered by TypeScript.
588 lines • 19.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Page = void 0;
const path_1 = __importDefault(require("path"));
const log_1 = require("../../../../pkg/log");
const pager_1 = require("./pager");
// Create domain-specific logger for page operations
const log = (0, log_1.getDomainLogger)('site', { component: 'page' });
/**
* Site Page - TypeScript equivalent of Go's Page entity in domain/site/entity/page.go
* This wraps the content layer's Page and provides site-specific functionality
*/
class Page {
constructor(tmplSvc, langSvc, publisher, contentPage, site) {
this.pageOutput = null;
// Resources from page sources
this.resources = [];
// Cache for paginator - CRITICAL: Must be synchronous for template access
this._paginator = null;
this.tmplSvc = tmplSvc;
this.langSvc = langSvc;
this.publisher = publisher;
this.contentPage = contentPage;
this.site = site;
}
/**
* Process page resources
*/
async processResources(pageSources) {
this.resources = pageSources;
}
/**
* Render the page - main entry point for page rendering
*/
async render() {
try {
await this.renderResources();
await this.renderPage();
}
catch (error) {
log.error(`Failed to render page ${this.paths().path()}: ${error}`);
}
}
getPageOutput() {
if (!this.pageOutput) {
this.pageOutput = this.output();
}
return this.pageOutput;
}
/**
* Render page content to HTML
* TypeScript equivalent of renderPage method from Go
*/
async renderPage() {
const layouts = this.layouts();
// Use real template lookup - exactly like Go version
const { preparer: tmpl, found } = await this.tmplSvc.lookupLayout(layouts);
if (!found) {
log.warn(`Failed to find layout: ${layouts.join(',')} for page ${this.paths().path()}`);
return;
}
const targetFilenames = [];
let prefix = this.getPageOutput().targetPrefix();
if (this.site.getLanguage().getCurrentLanguage() === prefix && prefix === this.langSvc.defaultLanguage()) {
prefix = '';
}
else {
prefix = this.site.getLanguage().getCurrentLanguage();
}
targetFilenames.push(path_1.default.join(prefix, this.getPageOutput().targetFilePath()));
await this.renderAndWritePage(tmpl, targetFilenames);
// Handle pagination if current exists - CRITICAL: This creates pagination directories
const current = await this.current();
if (current) {
let currentPager = current.next();
while (currentPager) {
this.setCurrent(currentPager);
await this.setupCurrentPaginator();
const paginationTargets = [path_1.default.join(prefix, currentPager.url(), this.getPageOutput().targetFileBase())];
await this.renderAndWritePage(tmpl, paginationTargets);
currentPager = currentPager.next();
}
}
}
/**
* Render and write page content to files
* TypeScript equivalent of renderAndWritePage method from Go
*/
async renderAndWritePage(tmpl, targetFilenames) {
try {
const renderedContent = await this.tmplSvc.executeWithContext(tmpl, this);
await this.publisher.publishSource(renderedContent, ...targetFilenames);
}
catch (error) {
throw this.errorf(error, 'failed to publish page');
}
}
/**
* Render page resources
* TypeScript equivalent of renderResources method from Go
*/
async renderResources() {
for (const pageSource of this.resources) {
const targetFilenames = [];
const output = this.getPageOutput();
let prefix = output.targetPrefix();
if (this.site.getLanguage().getCurrentLanguage() === prefix && prefix === this.langSvc.defaultLanguage()) {
prefix = '';
}
else {
prefix = this.site.getLanguage().getCurrentLanguage();
}
// For PageSource, we use the path as target path
targetFilenames.push(path_1.default.join(prefix, pageSource.path()));
let stream = null;
try {
// Create a readable stream from the page source
const opener = () => pageSource.pageFile().open();
stream = await opener();
if (!stream) {
throw new Error('Failed to open resource stream');
}
// Convert to ReadableStream if needed
let readableStream;
if (typeof stream.read === 'function') {
readableStream = new ReadableStream({
async start(controller) {
try {
const buffer = new Uint8Array(8192); // Increased buffer size to 8KB
// Loop to read entire file content
while (true) {
const result = await stream.read(buffer);
// If no bytes read, we've reached the end of file
if (result.bytesRead === 0) {
break;
}
// Enqueue the actual bytes read (not the full buffer)
controller.enqueue(buffer.slice(0, result.bytesRead));
}
controller.close();
}
catch (error) {
controller.error(error);
}
}
});
}
else {
// Fallback: create empty stream
readableStream = new ReadableStream({
start(controller) {
controller.close();
}
});
}
await this.publisher.publishFiles(readableStream, ...targetFilenames);
}
catch (error) {
throw this.errorf(error, 'failed to publish page resources');
}
finally {
if (stream) {
try {
await stream.close();
}
catch (closeError) {
log.warn(`Failed to close resource stream: ${closeError}`);
}
}
}
}
}
/**
* Create formatted error with context
* TypeScript equivalent of errorf method from Go
*/
errorf(err, format, ...args) {
const contextArgs = [this.pageIdentity().pageLanguage(), this.paths().path(), ...args];
const fullFormat = `[%s] page "%s": ${format}: %s`;
const message = this.sprintf(fullFormat, ...contextArgs, err.message || err);
return new Error(message);
}
/**
* Simple sprintf implementation
*/
sprintf(format, ...args) {
let i = 0;
return format.replace(/%s/g, () => args[i++] || '');
}
/**
* Clone the page
* TypeScript equivalent of clone method from Go
*/
clone() {
const cloned = new Page(this.tmplSvc, this.langSvc, this.publisher, this.contentPage, this.site);
// Copy resources
cloned.resources = [...this.resources];
cloned.pageOutput = this.pageOutput;
return cloned;
}
// =================================================================
// SIMPLE DELEGATION TO CONTENT LAYER - NO BUSINESS LOGIC HERE
// This follows golang's pattern where domain/site just wraps domain/contenthub
// =================================================================
// Implement ContentPage interface by delegating to contentPage
pageIdentity() {
return this.contentPage.pageIdentity();
}
pageFile() {
return this.contentPage.pageFile();
}
staleVersions() {
return this.contentPage.staleVersions();
}
section() {
return this.contentPage.section();
}
paths() {
return this.contentPage.paths();
}
path() {
return this.contentPage.path();
}
file() {
return this.contentPage.file();
}
name() {
return this.contentPage.name();
}
title() {
return this.contentPage.title();
}
kind() {
return this.contentPage.kind();
}
scratch() {
return this.contentPage.scratch();
}
IsAncestor(other) {
return this.contentPage.isAncestor(other.contentPage);
}
// Template-friendly properties (uppercase)
get Title() {
return this.title();
}
get Section() {
return this.contentPage.section();
}
get LinkTitle() {
return this.title();
}
get IsSection() {
return this.contentPage.isSection();
}
get IsPage() {
return this.contentPage.isPage();
}
get Content() {
return this.getPageOutput().content();
}
get Description() {
return this.description();
}
get Date() {
return this.pageDate();
}
get RelPermalink() {
// 获取基础的目标文件路径
let targetPath;
if (this.pageIdentity().pageLanguage() === this.langSvc.defaultLanguage()) {
targetPath = this.getPageOutput().targetFilePath();
}
else {
// 对于非默认语言,使用前缀 + 文件路径
const prefix = this.getPageOutput().targetPrefix();
const filePath = this.getPageOutput().targetFilePath();
targetPath = this.pathJoin(prefix, filePath);
}
if (targetPath.startsWith('/')) {
targetPath = targetPath.slice(1);
}
// 使用 site 的 URL 服务来生成相对 URL
// 这会根据 baseURL 的类型(相对或绝对)自动处理
return this.site.getURL().relURL(targetPath);
}
get GitInfo() {
return {};
}
get File() {
return {
BaseFileName: this.contentPage.file().baseFileName(),
Dir: this.contentPage.file().dir(),
};
}
isHome() {
return this.contentPage.isHome();
}
isPage() {
return this.contentPage.isPage();
}
isSection() {
return this.contentPage.isSection();
}
isAncestor(other) {
return this.contentPage.isAncestor(other);
}
eq(other) {
return this.contentPage.eq(other);
}
isBundled() {
return this.contentPage.isBundled();
}
layouts() {
return this.contentPage.layouts();
}
output() {
return this.contentPage.output();
}
pageOutputs() {
return [this.contentPage.output()];
}
truncated() {
return this.contentPage.truncated();
}
parent() {
return this.contentPage.parent();
}
pages() {
return this.contentPage.pages();
}
isStale() {
return this.contentPage.pageIdentity().isStale();
}
clearStale() {
this.contentPage.pageIdentity().clearStale();
}
prevInSection() {
return this.contentPage.prevInSection();
}
nextInSection() {
return this.contentPage.nextInSection();
}
sections(langIndex) {
return this.contentPage.sections(langIndex);
}
regularPages() {
return this.contentPage.regularPages();
}
regularPagesRecursive() {
return this.contentPage.regularPagesRecursive();
}
terms(langIndex, taxonomy) {
return this.contentPage.terms(langIndex, taxonomy);
}
isTranslated() {
return this.contentPage.isTranslated();
}
translations() {
return this.contentPage.translations();
}
rawContent() {
return this.contentPage.rawContent();
}
description() {
return this.contentPage.description ? this.contentPage.description() : '';
}
params() {
return this.contentPage.params ? this.contentPage.params() : {};
}
pageWeight() {
return this.contentPage.pageWeight ? this.contentPage.pageWeight() : 0;
}
pageDate() {
return this.contentPage.pageDate ? this.contentPage.pageDate() : new Date();
}
publishDate() {
return this.contentPage.publishDate ? this.contentPage.publishDate() : new Date();
}
async relatedKeywords(cfg) {
return this.contentPage.relatedKeywords ? await this.contentPage.relatedKeywords(cfg) : [];
}
shouldList(global) {
return this.contentPage.shouldList ? this.contentPage.shouldList(global) : true;
}
shouldListAny() {
return this.contentPage.shouldListAny ? this.contentPage.shouldListAny() : true;
}
noLink() {
return this.contentPage.noLink ? this.contentPage.noLink() : false;
}
// PagerManager delegation - the content layer handles all pagination logic
current() {
return this.contentPage.current();
}
setCurrent(current) {
this.contentPage.setCurrent(current);
}
posOffset(offset) {
return this.contentPage.posOffset(offset);
}
paginator() {
return this.contentPage.paginator();
}
async paginate(groups) {
return this.contentPage.paginate(groups);
}
async Summary() {
return this.contentPage.output().summary();
}
get Plain() {
return this.rawContent();
}
get TableOfContents() {
return this.getPageOutput().tableOfContents();
}
get Params() {
return this.params();
}
get Site() {
return this.site;
}
get Sites() {
return {
First: this.site,
Default: this.site,
};
}
get Lastmod() {
return this.contentPage.pageDate ? this.contentPage.pageDate() : null;
}
get Sitemap() {
return {
ChangeFreq: 'weekly',
Priority: 0.5,
Filename: 'sitemap.xml'
};
}
get IsTranslated() {
return this.isTranslated();
}
// Paginator property for templates - CRITICAL: Must be synchronous!
// Equivalent to golang's func (p *Page) Paginator() (*SitePager, error)
async Paginator() {
await this.setupCurrentPaginator();
return this._paginator;
}
async setupCurrentPaginator() {
try {
// Call contentPage.paginator() just like golang's p.Page.Paginator()
const contentPager = await this.contentPage.paginator();
if (contentPager) {
this._paginator = new pager_1.SitePager(this, contentPager);
}
else {
this._paginator = null;
}
}
catch (error) {
log.error('Error initializing Paginator:', error);
this._paginator = null;
}
}
get Language() {
return {
Lang: this.pageIdentity().pageLanguage(),
LanguageName: this.langSvc.getLanguageName(this.pageIdentity().pageLanguage()),
Title: this.title(),
Weight: this.pageWeight()
};
}
get Permalink() {
return this.joinURL(this.site.baseURL(), this.RelPermalink);
}
/**
* Site page conversion - convert content pages to site pages
* Equivalent to golang's func (p *Page) sitePages(ps contenthub.Pages) []*Page
*/
sitePages(ps) {
if (!ps) {
return [];
}
// Handle Pages interface with len() method
if (typeof ps === 'object' && typeof ps.len === 'function') {
const pages = [];
const length = ps.len();
for (let i = 0; i < length; i++) {
const cp = ps[i];
if (cp) {
const sp = this.sitePage(cp);
if (sp) {
pages.push(sp);
}
}
}
return pages;
}
// Handle array directly
if (Array.isArray(ps)) {
const pages = [];
for (const cp of ps) {
const sp = this.sitePage(cp);
if (sp) {
pages.push(sp);
}
}
return pages;
}
return [];
}
/**
* Convert single content page to site page
* Equivalent to golang's func (p *Page) sitePage(cp contenthub.Page) (*Page, error)
*/
sitePage(p) {
try {
if (p === null) {
log.warn(`Invalid content page provided: ${p}`);
return null;
}
return new Page(this.tmplSvc, this.langSvc, this.publisher, p, this.site);
}
catch (error) {
log.error(`Error creating site page: ${error}`);
return null;
}
}
// Utility methods
pathJoin(...paths) {
return paths
.filter(p => p && p.length > 0)
.map(p => p.replace(/^\/+|\/+$/g, ''))
.filter(p => p.length > 0)
.join('/');
}
joinURL(baseURL, targetPath) {
if (!baseURL)
return targetPath;
if (!targetPath)
return baseURL;
// Remove trailing slash from baseURL
if (baseURL.endsWith('/')) {
baseURL = baseURL.slice(0, -1);
}
// Ensure targetPath starts with /
if (!targetPath.startsWith('/')) {
targetPath = '/' + targetPath;
}
return baseURL + targetPath;
}
get Translations() {
const translations = this.translations();
return Array.isArray(translations) ? translations.map(t => this.sitePage(t)).filter(p => p !== null) : [];
}
async GetTerms(taxonomy) {
const terms = await this.terms(this.site.getLanguage().currentLanguageIndex(), taxonomy);
return Array.isArray(terms) ? terms.map(t => this.sitePage(t)).filter(p => p !== null) : [];
}
async Pages() {
return await this.getSitePages();
}
get Page() {
return this.contentPage;
}
get Kind() {
return this.kind();
}
get Parent() {
return this.sitePage(this.contentPage.parent());
}
/**
* Get site pages method - following Go's Page.Pages() pattern
* Equivalent to golang's func (p *Page) Pages() []*Page
*/
async getSitePages() {
// Get pages from contentPage (equivalent to p.Page.Pages() in Go)
const ps = await this.contentPage.pages();
if (!ps || (Array.isArray(ps) && ps.length === 0)) {
return [];
}
// Convert to site pages (equivalent to p.sitePages(ps) in Go)
return this.sitePages(ps);
}
}
exports.Page = Page;
//# sourceMappingURL=page.js.map