From d4be18a1cce589ba3f2ec51ef28efa12b7700cc8 Mon Sep 17 00:00:00 2001 From: gerteck Date: Thu, 25 Dec 2025 02:00:06 +0800 Subject: [PATCH 01/14] Add jsdoc for template property --- packages/core/src/Page/PageConfig.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/src/Page/PageConfig.ts b/packages/core/src/Page/PageConfig.ts index 82a67b3031..6d37f73134 100644 --- a/packages/core/src/Page/PageConfig.ts +++ b/packages/core/src/Page/PageConfig.ts @@ -65,6 +65,11 @@ export class PageConfig { */ src: string; title?: string; + /** + * The compiled Nunjucks template for the page wrapper (page.njk). + * This is used to generate the HTML version of the page, handling the global structure + * (html, head, body, scripts) wrapping the processed content. + */ template: Template; variableProcessor: VariableProcessor; addressablePagesSource: string[]; From 72df4bdd7be1ca3313634c4e4e970715a69d8819 Mon Sep 17 00:00:00 2001 From: gerteck Date: Thu, 25 Dec 2025 19:59:33 +0800 Subject: [PATCH 02/14] Allow custom file types Generated with Nunjucks variable processing, Configured in `site.json`. --- .../core/src/Page/PageVueServerRenderer.ts | 2 +- packages/core/src/Page/index.ts | 34 +++++++++++++++++-- packages/core/src/Site/SiteConfig.ts | 1 + packages/core/src/Site/index.ts | 21 ++++++++---- 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/packages/core/src/Page/PageVueServerRenderer.ts b/packages/core/src/Page/PageVueServerRenderer.ts index 3d42897f41..b2f52e0786 100644 --- a/packages/core/src/Page/PageVueServerRenderer.ts +++ b/packages/core/src/Page/PageVueServerRenderer.ts @@ -138,7 +138,7 @@ Bundle is regenerated by webpack and built pages are re-rendered with the latest Object.values(pageEntries).forEach(async (pageEntry) => { const { page, renderFn } = pageEntry; const renderedVuePageContent = await renderVuePage(renderFn); - page.outputPageHtml(renderedVuePageContent); + page.writeOutputFile(renderedVuePageContent); }); } diff --git a/packages/core/src/Page/index.ts b/packages/core/src/Page/index.ts index f4a0db76c1..d273dc3bf5 100644 --- a/packages/core/src/Page/index.ts +++ b/packages/core/src/Page/index.ts @@ -505,6 +505,16 @@ export class Page { return ''; } + /** + * Generates the page content by processing variables, nodes, frontmatter, and plugins. + * + * If the file extension is not .html, it simply processes variables and writes the content. + * + * For HTML files (usual case), it also builds the page navigation, handles layout, + * collects headings/keywords, and compiles the page into a Vue application for server-side rendering. + * Finally, it writes the rendered HTML to the output file. + * @param externalManager to manage external dependencies and configuration + */ async generate(externalManager: ExternalManager) { this.resetState(); // Reset for live reload @@ -531,6 +541,12 @@ export class Page { pluginManager, siteLinkManager, this.pageUserScriptsAndStyles); let content = variableProcessor.renderWithSiteVariables(this.pageConfig.sourcePath, pageSources); + + if (path.extname(this.pageConfig.resultPath) !== '.html') { + await this.writeOutputFile(content); + return; + } + content = await nodeProcessor.process(this.pageConfig.sourcePath, content) as string; this.processFrontmatter(nodeProcessor.frontmatter); content = pluginManager.postRender(this.frontmatter, content); @@ -574,13 +590,25 @@ export class Page { this.filterIconAssets(content, vueSsrHtml); if (process.env.TEST_MODE) { content = `
${content}
`; - await this.outputPageHtml(content); + await this.writeOutputFile(content); } else { - await this.outputPageHtml(vueSsrHtml); + await this.writeOutputFile(vueSsrHtml); } } - async outputPageHtml(content: string) { + /** + * Writes the rendered content to the output file. + * For non-HTML files, it writes the content directly. + * For HTML files, it renders the content into the page template (page.njk) before writing. + * @param content The processed page content (with variables substituted, etc.) + */ + async writeOutputFile(content: string) { + if (path.extname(this.pageConfig.resultPath) !== '.html') { + await fs.outputFile(this.pageConfig.resultPath, content); + return; + } + + // Prepare data for page.njk template const renderedTemplate = this.pageConfig.template.render( this.prepareTemplateData(content)); // page.njk diff --git a/packages/core/src/Site/SiteConfig.ts b/packages/core/src/Site/SiteConfig.ts index f7a4bcb60a..5d7a844fd0 100644 --- a/packages/core/src/Site/SiteConfig.ts +++ b/packages/core/src/Site/SiteConfig.ts @@ -13,6 +13,7 @@ export type SiteConfigPage = { globExclude?: string, searchable?: string | boolean, frontmatter?: FrontMatter, + fileExtension?: string, }; export type SiteConfigStyle = { diff --git a/packages/core/src/Site/index.ts b/packages/core/src/Site/index.ts index e8b861f4ab..5f927a117c 100644 --- a/packages/core/src/Site/index.ts +++ b/packages/core/src/Site/index.ts @@ -91,23 +91,25 @@ const MARKBIND_LINK_HTML = `MarkBind ${MARKBIN type PageCreationConfig = { externalScripts: string[], frontmatter: FrontMatter, - layout: string, + layout?: string, pageSrc: string, searchable: boolean, faviconUrl?: string, glob?: string, globExclude?: string title?: string, + fileExtension?: string, }; type AddressablePage = { - frontmatter: FrontMatter, - layout: string, - searchable: string, + frontmatter?: FrontMatter, + layout?: string, + searchable?: string | boolean, src: string, externalScripts?: string[], faviconUrl?: string, title?: string, + fileExtension?: string, }; type PageGenerationTask = { @@ -280,7 +282,10 @@ export class Site { */ createPage(config: PageCreationConfig): Page { const sourcePath = path.join(this.rootPath, config.pageSrc); - const resultPath = path.join(this.outputPath, fsUtil.setExtension(config.pageSrc, '.html')); + const outputExtension = config.fileExtension || '.html'; + const relativePath = path.posix.relative(this.rootPath, sourcePath); + const outputPath = fsUtil.setExtension(relativePath, outputExtension); + const resultPath = path.join(this.outputPath, outputPath); const baseAssetsPath = path.posix.join( this.siteConfig.baseUrl || '/', TEMPLATE_SITE_ASSET_FOLDER_NAME, @@ -385,6 +390,7 @@ export class Site { searchable: page.searchable, layout: page.layout, frontmatter: page.frontmatter, + ...(page.fileExtension && { fileExtension: page.fileExtension }), }))) as AddressablePage[]; /* Add pages collected from globs and merge properties for pages @@ -938,9 +944,10 @@ export class Site { pageSrc: page.src, title: page.title, layout: page.layout, - frontmatter: page.frontmatter, - searchable: page.searchable !== 'no', + frontmatter: page.frontmatter || {}, + searchable: page.searchable !== 'no' && page.searchable !== false, externalScripts: page.externalScripts || [], + fileExtension: page.fileExtension, }); } From 72941306869133f7eecf47ad627cab534ace9f6c Mon Sep 17 00:00:00 2001 From: gerteck Date: Fri, 26 Dec 2025 01:54:24 +0800 Subject: [PATCH 03/14] Add docs for custom file type feature --- docs/site.json | 10 ++++ docs/userGuide/addingPages.md | 51 ++++++++++++++++++++ docs/userGuide/customFileTypes/sampleJson.md | 7 +++ docs/userGuide/customFileTypes/sampleTxt.md | 8 +++ docs/userGuide/siteJsonFile.md | 1 + docs/userGuide/syntax/links.md | 2 +- 6 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 docs/userGuide/customFileTypes/sampleJson.md create mode 100644 docs/userGuide/customFileTypes/sampleTxt.md diff --git a/docs/site.json b/docs/site.json index 89193d5e29..f478492450 100644 --- a/docs/site.json +++ b/docs/site.json @@ -6,6 +6,16 @@ "codeTheme": "light" }, "pages": [ + { + "src": "userGuide/customFileTypes/sampleJson.md", + "fileExtension": ".json", + "searchable": "no" + }, + { + "src": "userGuide/customFileTypes/sampleTxt.md", + "fileExtension": ".txt", + "searchable": "no" + }, { "glob": ["*.md", "userGuide/*.md", "userGuide/components/*.md", "devGuide/*.md", "devGuide/*/*.md"] }, diff --git a/docs/userGuide/addingPages.md b/docs/userGuide/addingPages.md index 38e31524eb..37480b70d4 100644 --- a/docs/userGuide/addingPages.md +++ b/docs/userGuide/addingPages.md @@ -94,5 +94,56 @@ in your `site.json` file. See the [`pagesExclude` attribute](siteJsonFile.html#p +## Generating Custom File Types + +You can also use MarkBind to generate non-HTML files, such as `.json` or `.txt`, from your source `.md` files. This is useful for generating dynamic configuration files or other text-based assets that can benefit from MarkBind's variable system. + +To do this, specify the `fileExtension` property for the page configuration in your `site.json`, as explained [here](siteJsonFile.html#pages). + +
+ +{{ icon_example }} Generating a `sampleJson.json` from `sampleJson.md`: + +**site.json:** +```json +{ + "pages": [ + { + "src": "userGuide/customFileTypes/sampleJson.md", + "fileExtension": ".json", + "searchable": "no" + } + ] +} +``` +**sampleJson.md:** + +{% raw %} +```md +{ + "title": "globalVariables", + "description": "This file is served as a .json file, but is sourced from a .md file in the source code.", + "baseUrl": "{{baseUrl}}", + "timestamp": "{{timestamp}}", + "MarkBind": "{{MarkBind}}" +} +``` +{% endraw %} + + +**Examples of generated custom files:** +* [sampleJson]({{baseUrl}}/userGuide/customFileTypes/sampleJson.json){no-validation} - A JSON file generated from Markdown, utilizing default global nunjucks variables available in MarkBind sites. +* [sampleTxt]({{baseUrl}}/userGuide/customFileTypes/sampleTxt.txt){no-validation} - A plain text file generated from Markdown, also utilizing global variables. + + +
+ +**Key Points:** +* **Nunjucks Variables**: You can use Nunjucks global variables (like `baseUrl`, `timestamp`) and site variables within these files, just like in your HTML pages. +* **No Frontmatter or Scripts**: Unlike standard MarkBind pages, custom file types **do not support frontmatter or scripts**. Frontmatter and script tags will be treated as plain text if included. +* **Search**: By default, these files are searchable if `enableSearch` is true for the site. You can disable this by setting `"searchable": "no"` (or `false`) in the page config. +* **Intra-site validation**: If you are linking such files in other pages in your site, you can use the `{no-validation}` tag, e.g. `[sampleTxt]({{baseUrl}}/userGuide/customFileTypes/sampleTxt.txt){no-validation}` to disable intra-site validation warnings for the link, as MarkBind currently does not support intra-site validation for custom file type links. + + {% from "njk/common.njk" import previous_next %} {{ previous_next('authoringContents', 'markBindSyntaxOverview') }} diff --git a/docs/userGuide/customFileTypes/sampleJson.md b/docs/userGuide/customFileTypes/sampleJson.md new file mode 100644 index 0000000000..c3c2130bd3 --- /dev/null +++ b/docs/userGuide/customFileTypes/sampleJson.md @@ -0,0 +1,7 @@ +{ + "title": "globalVariables", + "description": "This file is served as a .json file, but is sourced from a .md file in the source code.", + "baseUrl": "{{baseUrl}}", + "timestamp": "{{timestamp}}", + "MarkBind": "{{MarkBind}}" +} \ No newline at end of file diff --git a/docs/userGuide/customFileTypes/sampleTxt.md b/docs/userGuide/customFileTypes/sampleTxt.md new file mode 100644 index 0000000000..8911c3f117 --- /dev/null +++ b/docs/userGuide/customFileTypes/sampleTxt.md @@ -0,0 +1,8 @@ +This file is served as a .txt file, sourced from a .md file. + +The content below demonstrates nunjucks variables usage for custom file types: + + +{% raw %} {{ baseUrl }} {% endraw %}: "{{ baseUrl }}" +{% raw %} {{ timestamp }} {% endraw %}: "{{ timestamp }}" +{% raw %} {{ MarkBind }} {% endraw %}: ”{{ MarkBind }}“ diff --git a/docs/userGuide/siteJsonFile.md b/docs/userGuide/siteJsonFile.md index 1ab30aed76..d1a740baea 100644 --- a/docs/userGuide/siteJsonFile.md +++ b/docs/userGuide/siteJsonFile.md @@ -138,6 +138,7 @@ _(Optional)_ **The styling options to be applied to the site.** This includes: * **`searchable`**: Specifies that the page(s) should be excluded from searching. Default: `yes`. * **`externalScripts`**: An array of external scripts to be referenced on the page. Scripts referenced will be run before the layout script. * **`frontmatter`**: Specifies properties to add to the frontmatter of a page or glob of pages. Overrides any existing properties if they have the same name, and overrides any frontmatter properties specified in `globalOverride`. +* **`fileExtension`**: A string that specifies the output file extension (e.g., `".json"`, `".txt"`) for the generated file. If not specified, defaults to `".html"`. Note that non-HTML files do not support frontmatter or scripts.
diff --git a/docs/userGuide/syntax/links.md b/docs/userGuide/syntax/links.md index 3501276766..34bfc60f46 100644 --- a/docs/userGuide/syntax/links.md +++ b/docs/userGuide/syntax/links.md @@ -65,7 +65,7 @@ Links to files of the generated site (e.g., an HTML page or an image file) can b
-You may link to markdown files using its original extension (**.md**) as it will automatically be converted to a HTML extension (**.html**) when the site is generated. +You may link to markdown files using their original extension (**.md**). They will automatically be converted to the configured output extension (default **.html**) when the site is generated. {{ icon_example }} `Click [here](index.md)` --- *auto-conversion* ---> `Click [here](index.html)` From bda46b791b289516705c229683a83422b54ac3ac Mon Sep 17 00:00:00 2001 From: gerteck Date: Fri, 26 Dec 2025 02:22:11 +0800 Subject: [PATCH 04/14] Revert doc change --- docs/userGuide/syntax/links.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userGuide/syntax/links.md b/docs/userGuide/syntax/links.md index 34bfc60f46..d22dd44f72 100644 --- a/docs/userGuide/syntax/links.md +++ b/docs/userGuide/syntax/links.md @@ -65,7 +65,7 @@ Links to files of the generated site (e.g., an HTML page or an image file) can b
-You may link to markdown files using their original extension (**.md**). They will automatically be converted to the configured output extension (default **.html**) when the site is generated. +You may link to markdown files using its original extension (**.md**) as it will automatically be converted to a HTML extension (**.html**) when the site is generated. {{ icon_example }} `Click [here](index.md)` --- *auto-conversion* ---> `Click [here](index.html)` From 6c2127ba2e8e984fb0bd67980ec1ffae49153d4a Mon Sep 17 00:00:00 2001 From: gerteck Date: Fri, 26 Dec 2025 11:13:21 +0800 Subject: [PATCH 05/14] Ensure correct relative paths --- packages/core/src/Site/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/Site/index.ts b/packages/core/src/Site/index.ts index 5f927a117c..cb6e9a0085 100644 --- a/packages/core/src/Site/index.ts +++ b/packages/core/src/Site/index.ts @@ -283,7 +283,7 @@ export class Site { createPage(config: PageCreationConfig): Page { const sourcePath = path.join(this.rootPath, config.pageSrc); const outputExtension = config.fileExtension || '.html'; - const relativePath = path.posix.relative(this.rootPath, sourcePath); + const relativePath = fsUtil.ensurePosix(path.relative(this.rootPath, sourcePath)); const outputPath = fsUtil.setExtension(relativePath, outputExtension); const resultPath = path.join(this.outputPath, outputPath); From f0f87968dac5a05f47154e44dbd3501aa4bd7de4 Mon Sep 17 00:00:00 2001 From: gerteck Date: Fri, 26 Dec 2025 11:13:29 +0800 Subject: [PATCH 06/14] Simplify `fileExtension` property assignment --- packages/core/src/Site/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/Site/index.ts b/packages/core/src/Site/index.ts index cb6e9a0085..668ebef394 100644 --- a/packages/core/src/Site/index.ts +++ b/packages/core/src/Site/index.ts @@ -390,7 +390,7 @@ export class Site { searchable: page.searchable, layout: page.layout, frontmatter: page.frontmatter, - ...(page.fileExtension && { fileExtension: page.fileExtension }), + fileExtension: page.fileExtension, }))) as AddressablePage[]; /* Add pages collected from globs and merge properties for pages From 28318383cf0acc375536ef4f38e744a96cb13bd6 Mon Sep 17 00:00:00 2001 From: gerteck Date: Fri, 26 Dec 2025 11:15:32 +0800 Subject: [PATCH 07/14] Revert doc --- docs/userGuide/syntax/links.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userGuide/syntax/links.md b/docs/userGuide/syntax/links.md index d22dd44f72..3501276766 100644 --- a/docs/userGuide/syntax/links.md +++ b/docs/userGuide/syntax/links.md @@ -65,7 +65,7 @@ Links to files of the generated site (e.g., an HTML page or an image file) can b
-You may link to markdown files using its original extension (**.md**) as it will automatically be converted to a HTML extension (**.html**) when the site is generated. +You may link to markdown files using its original extension (**.md**) as it will automatically be converted to a HTML extension (**.html**) when the site is generated. {{ icon_example }} `Click [here](index.md)` --- *auto-conversion* ---> `Click [here](index.html)` From 9521cae64adde71dee9dcfa68117f823893de71c Mon Sep 17 00:00:00 2001 From: gerteck Date: Fri, 26 Dec 2025 11:19:57 +0800 Subject: [PATCH 08/14] Add testcase for site.json --- packages/core/test/unit/Site.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/core/test/unit/Site.test.ts b/packages/core/test/unit/Site.test.ts index 7288ae698b..a96dc607dd 100644 --- a/packages/core/test/unit/Site.test.ts +++ b/packages/core/test/unit/Site.test.ts @@ -547,6 +547,22 @@ const siteJsonResolvePropertiesTestCases = [ }, ], }, + { + name: 'Site.json passes fileExtension property', + pages: [ + { + glob: '*.md', + fileExtension: '.json', + }, + ], + expected: [ + { + src: 'index.md', + fileExtension: '.json', + searchable: undefined, + }, + ], + }, ]; siteJsonResolvePropertiesTestCases.forEach((testCase) => { From 84b6b3f9cdebae1e0a9ed4e5b1920b4970725774 Mon Sep 17 00:00:00 2001 From: gerteck Date: Fri, 26 Dec 2025 11:33:50 +0800 Subject: [PATCH 09/14] test: Add tests for fileExtension property --- packages/core/test/unit/Site.test.ts | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/core/test/unit/Site.test.ts b/packages/core/test/unit/Site.test.ts index a96dc607dd..30dd10ad3e 100644 --- a/packages/core/test/unit/Site.test.ts +++ b/packages/core/test/unit/Site.test.ts @@ -563,6 +563,22 @@ const siteJsonResolvePropertiesTestCases = [ }, ], }, + { + name: 'Site.json merges valid fileExtension property with src', + pages: [ + { + src: 'index.md', + fileExtension: '.json', + }, + ], + expected: [ + { + src: 'index.md', + fileExtension: '.json', + searchable: undefined, + }, + ], + }, ]; siteJsonResolvePropertiesTestCases.forEach((testCase) => { @@ -699,3 +715,31 @@ siteJsonPageExclusionTestCases.forEach((testCase) => { .toEqual(testCase.expected); }); }); + +test('createPage generates correct page config with fileExtension', async () => { + const json = { + ...PAGE_NJK, + 'site.json': SITE_JSON_DEFAULT, + 'test.md': '', + }; + mockFs.vol.fromJSON(json, ''); + + const site = new Site(...siteArguments); + await site.readSiteConfig(); + const config = { + pageSrc: 'test.md', + title: 'Test Page', + fileExtension: '.json', + searchable: true, + frontmatter: {}, + externalScripts: [], + }; + site.createPage(config); + + // Page is mocked + const PageMock = jest.requireMock('../../src/Page').Page; + const pageConfig = PageMock.mock.calls[0][0]; + + expect(pageConfig.resultPath).toMatch(/test\.json$/); + expect(pageConfig.sourcePath).toMatch(/test\.md$/); +}); From 86817506f3cbfc76df9a61d1271f4fefd7d82a80 Mon Sep 17 00:00:00 2001 From: gerteck Date: Fri, 26 Dec 2025 11:51:17 +0800 Subject: [PATCH 10/14] test: add unit tests for Page class's generate method and Site's createNewPage method --- packages/core/test/unit/Page/index.test.ts | 72 ++++++++++++++++++++++ packages/core/test/unit/Site.test.ts | 31 ++++++++++ 2 files changed, 103 insertions(+) create mode 100644 packages/core/test/unit/Page/index.test.ts diff --git a/packages/core/test/unit/Page/index.test.ts b/packages/core/test/unit/Page/index.test.ts new file mode 100644 index 0000000000..8de207c89d --- /dev/null +++ b/packages/core/test/unit/Page/index.test.ts @@ -0,0 +1,72 @@ +import path from 'path'; +import fs from 'fs-extra'; +import { Page } from '../../../src/Page'; + +const mockFs = fs as any; + +jest.mock('fs'); +jest.mock('walk-sync'); +jest.mock('../../../src/Page/PageVueServerRenderer'); +jest.mock('../../../src/html/NodeProcessor'); +jest.mock('../../../src/variables/VariableProcessor'); +jest.mock('../../../src/plugins/PluginManager'); +jest.mock('../../../src/html/SiteLinkManager'); +jest.mock('../../../src/Layout/LayoutManager'); +jest.mock('../../../src/Page/PageSources'); +jest.mock('../../../src/External/ExternalManager'); + +describe('Page', () => { + const pageConfig = { + sourcePath: 'test.md', + resultPath: 'test.json', + }; + const siteConfig = { + baseUrl: '/', + style: { + codeLineNumbers: false, + }, + ignore: [], + intrasiteLinkValidation: {}, + plantumlCheck: false, + }; + + test('generate writes raw content for non-html files', async () => { + mockFs.vol.fromJSON({ + 'test.md': '{"foo": "bar"}', + }); + + const VariableProcessor = require('../../../src/variables/VariableProcessor').VariableProcessor; + // Mock the VariableProcessor instance to return a specific render method. + // This is necessary because Page.generate uses VariableProcessor internally. + VariableProcessor.mockImplementation(() => ({ + renderWithSiteVariables: jest.fn(() => '{"foo": "bar"}'), + })); + + // Inject the mocked variableProcessor into the Page configuration. + // The Page constructor assigns this.pageConfig from the passed argument, + // and subsequent methods use this.pageConfig.variableProcessor. + const variableProcessor = { + renderWithSiteVariables: jest.fn(() => '{"foo": "bar"}'), + }; + + const configWithMock = { + ...pageConfig, + variableProcessor, + }; + + const page = new Page(configWithMock as any, siteConfig as any); + + const externalManager = { + config: { outputPath: '_site' }, + generateDependencies: jest.fn(), + }; + + await page.generate(externalManager as any); + + // Verify fs.outputFile was called with correct content + // Verify file content in mocked filesystem + const content = fs.readFileSync(path.resolve('test.json'), 'utf8'); + expect(content).toEqual('{"foo": "bar"}'); + + }); +}); diff --git a/packages/core/test/unit/Site.test.ts b/packages/core/test/unit/Site.test.ts index 30dd10ad3e..922cdb0a6f 100644 --- a/packages/core/test/unit/Site.test.ts +++ b/packages/core/test/unit/Site.test.ts @@ -743,3 +743,34 @@ test('createPage generates correct page config with fileExtension', async () => expect(pageConfig.resultPath).toMatch(/test\.json$/); expect(pageConfig.sourcePath).toMatch(/test\.md$/); }); + +test('createNewPage generates correct page config', async () => { + const json = { + ...PAGE_NJK, + 'site.json': SITE_JSON_DEFAULT, + 'test.md': '', + }; + mockFs.vol.fromJSON(json, ''); + + const site = new Site(...siteArguments); + await site.readSiteConfig(); + + const page = { + src: 'test.md', + title: 'Test Page', + layout: 'default', + frontmatter: {}, + searchable: true, + fileExtension: '.json', + }; + + site.createNewPage(page as any, undefined); + + // Page is mocked, retrieve the last call to the Page constructor + const PageMock = jest.requireMock('../../src/Page').Page; + const lastCallIndex = PageMock.mock.calls.length - 1; + const lastPageConfig = PageMock.mock.calls[lastCallIndex][0]; + + expect(lastPageConfig.resultPath).toMatch(/test\.json$/); + expect(lastPageConfig.sourcePath).toMatch(/test\.md$/); +}); From 615d1fbd9388be335bc35a0206f5f84a21893172 Mon Sep 17 00:00:00 2001 From: gerteck Date: Fri, 26 Dec 2025 11:57:05 +0800 Subject: [PATCH 11/14] Fix lints --- packages/core/test/unit/Page/index.test.ts | 25 +++++++++++----------- packages/core/test/unit/Site.test.ts | 4 ++-- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/core/test/unit/Page/index.test.ts b/packages/core/test/unit/Page/index.test.ts index 8de207c89d..f4470000e6 100644 --- a/packages/core/test/unit/Page/index.test.ts +++ b/packages/core/test/unit/Page/index.test.ts @@ -1,6 +1,7 @@ import path from 'path'; import fs from 'fs-extra'; import { Page } from '../../../src/Page'; +import { VariableProcessor } from '../../../src/variables/VariableProcessor'; const mockFs = fs as any; @@ -31,33 +32,32 @@ describe('Page', () => { }; test('generate writes raw content for non-html files', async () => { - mockFs.vol.fromJSON({ - 'test.md': '{"foo": "bar"}', - }); + mockFs.vol.fromJSON({ + 'test.md': '{"foo": "bar"}', + }); - const VariableProcessor = require('../../../src/variables/VariableProcessor').VariableProcessor; // Mock the VariableProcessor instance to return a specific render method. // This is necessary because Page.generate uses VariableProcessor internally. - VariableProcessor.mockImplementation(() => ({ - renderWithSiteVariables: jest.fn(() => '{"foo": "bar"}'), + (VariableProcessor as unknown as jest.Mock).mockImplementation(() => ({ + renderWithSiteVariables: jest.fn().mockReturnValue('{"foo": "bar"}'), })); // Inject the mocked variableProcessor into the Page configuration. // The Page constructor assigns this.pageConfig from the passed argument, // and subsequent methods use this.pageConfig.variableProcessor. const variableProcessor = { - renderWithSiteVariables: jest.fn(() => '{"foo": "bar"}'), + renderWithSiteVariables: jest.fn().mockReturnValue('{"foo": "bar"}'), }; - + const configWithMock = { - ...pageConfig, - variableProcessor, + ...pageConfig, + variableProcessor, }; - + const page = new Page(configWithMock as any, siteConfig as any); const externalManager = { - config: { outputPath: '_site' }, + config: { outputPath: '_site' }, generateDependencies: jest.fn(), }; @@ -67,6 +67,5 @@ describe('Page', () => { // Verify file content in mocked filesystem const content = fs.readFileSync(path.resolve('test.json'), 'utf8'); expect(content).toEqual('{"foo": "bar"}'); - }); }); diff --git a/packages/core/test/unit/Site.test.ts b/packages/core/test/unit/Site.test.ts index 922cdb0a6f..dfea1d90ea 100644 --- a/packages/core/test/unit/Site.test.ts +++ b/packages/core/test/unit/Site.test.ts @@ -754,7 +754,7 @@ test('createNewPage generates correct page config', async () => { const site = new Site(...siteArguments); await site.readSiteConfig(); - + const page = { src: 'test.md', title: 'Test Page', @@ -763,7 +763,7 @@ test('createNewPage generates correct page config', async () => { searchable: true, fileExtension: '.json', }; - + site.createNewPage(page as any, undefined); // Page is mocked, retrieve the last call to the Page constructor From 3256c4a5eedf9f133788806166d17cd871112aa2 Mon Sep 17 00:00:00 2001 From: gerteck Date: Fri, 26 Dec 2025 12:12:47 +0800 Subject: [PATCH 12/14] Update docs --- docs/userGuide/addingPages.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/userGuide/addingPages.md b/docs/userGuide/addingPages.md index 37480b70d4..b36d460e37 100644 --- a/docs/userGuide/addingPages.md +++ b/docs/userGuide/addingPages.md @@ -132,16 +132,16 @@ To do this, specify the `fileExtension` property for the page configuration in y **Examples of generated custom files:** -* [sampleJson]({{baseUrl}}/userGuide/customFileTypes/sampleJson.json){no-validation} - A JSON file generated from Markdown, utilizing default global nunjucks variables available in MarkBind sites. -* [sampleTxt]({{baseUrl}}/userGuide/customFileTypes/sampleTxt.txt){no-validation} - A plain text file generated from Markdown, also utilizing global variables. +* [sampleJson]({{baseUrl}}/userGuide/customFileTypes/sampleJson.json){no-validation} - A JSON file generated from Markdown, utilizing default global nunjucks variables available in MarkBind sites. +* [sampleTxt]({{baseUrl}}/userGuide/customFileTypes/sampleTxt.txt){no-validation} - A plain text file generated from Markdown, also utilizing global variables.
**Key Points:** -* **Nunjucks Variables**: You can use Nunjucks global variables (like `baseUrl`, `timestamp`) and site variables within these files, just like in your HTML pages. -* **No Frontmatter or Scripts**: Unlike standard MarkBind pages, custom file types **do not support frontmatter or scripts**. Frontmatter and script tags will be treated as plain text if included. -* **Search**: By default, these files are searchable if `enableSearch` is true for the site. You can disable this by setting `"searchable": "no"` (or `false`) in the page config. +* **Nunjucks Variables**: You can use Nunjucks global variables (like `baseUrl`, `timestamp`) and site variables within these files, just like in your HTML pages. +* **No Frontmatter or Scripts**: Unlike standard MarkBind pages, custom file types **do not support frontmatter or scripts**. Frontmatter and script tags will be treated as plain text if included. +* **Search**: By default, these files are searchable if `enableSearch` is true for the site. You can disable this by setting `"searchable": "no"` (or `false`) in the page config. * **Intra-site validation**: If you are linking such files in other pages in your site, you can use the `{no-validation}` tag, e.g. `[sampleTxt]({{baseUrl}}/userGuide/customFileTypes/sampleTxt.txt){no-validation}` to disable intra-site validation warnings for the link, as MarkBind currently does not support intra-site validation for custom file type links. From efb664395f9a8e29fc84543ae8a12f652bc40be1 Mon Sep 17 00:00:00 2001 From: gerteck Date: Fri, 26 Dec 2025 12:13:05 +0800 Subject: [PATCH 13/14] Add logger warnings --- packages/core/src/Page/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/src/Page/index.ts b/packages/core/src/Page/index.ts index d273dc3bf5..57b40e660d 100644 --- a/packages/core/src/Page/index.ts +++ b/packages/core/src/Page/index.ts @@ -543,6 +543,12 @@ export class Page { let content = variableProcessor.renderWithSiteVariables(this.pageConfig.sourcePath, pageSources); if (path.extname(this.pageConfig.resultPath) !== '.html') { + const hasFrontmatterLike = /^\s*---\s*[\s\S]*?---/m.test(content); + const hasScriptTagLike = /]/i.test(content); + if (hasFrontmatterLike || hasScriptTagLike) { + logger.warn(`Detected frontmatter or