UNPKG

@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
"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