diff --git a/.gitignore b/.gitignore index 5ecb155..925b86e 100644 --- a/.gitignore +++ b/.gitignore @@ -121,4 +121,4 @@ __artifacts__ .DS_Store tests_output/ -nightwatch/ \ No newline at end of file +nightwatch/ diff --git a/README.md b/README.md index cf0d89c..741a161 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,36 @@ and then run `npx postdoc init` to create a new Postdoc project. Visit [the official documentation](https://postdoc.dev). + + +```shell + _____ _ _ + | __ \ | | | | + | |__) |__ ___| |_ __| | ___ ___ + | ___/ _ \/ __| __/ _` |/ _ \ / __| + | | | (_) \__ \ || (_| | (_) | (__ + |_| \___/|___/\__\__,_|\___/ \___| + + +─────────────────────────────────────── + +Usage: postdoc [options] [command] + +Options: + --version / -v outputs the current Postdoc version. + --help / -h display help for command + +Commands: + run Start the Vite-powered development server with live preview and Hot Module Replacement (HMR). + init Initializes a new Postdoc project and copies the necessary assets to start with. + test Runs all test declared in the project. + build Builds the project, copies assets into an output directory. + import Creates new pages and minimal tests from a CLI project. + create + preview Starts a simple static server over the output directory. + +``` + ## Support Having trouble? Get help in the [Discussions](https://github.com/PostDocJS/postdoc/discussions) or [create an issue](https://github.com/PostDocJS/postdoc/issues). diff --git a/jsconfig.json b/jsconfig.json index d669182..914aa59 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -2,5 +2,5 @@ "compilerOptions": { "moduleResolution": "node16" }, - "include": ["lib/**/*", "bin/*", "client/*"] + "include": ["lib/**/*", "bin/*", "client/*", "documentation"] } diff --git a/lib/collector.js b/lib/collector.js index 63c9b31..e382a11 100644 --- a/lib/collector.js +++ b/lib/collector.js @@ -9,16 +9,19 @@ import { pipeWith } from 'pipe-ts'; import Logger from './logger.js'; import ApiPage from './api-page.js'; import LayoutPage from './layout-page.js'; -import RegularPage from './regular-page.js'; +import MarkdownPage from './markdown-page.js'; +import RSTPage from './rst-page.js'; import PostDocError from './error.js'; import Configuration from './configuration.js'; import MarkdownCompiler from './markdown-compiler.js'; +import RSTCompiler from './rst-compiler.js'; import { walkDirectory } from './fs.js'; export default class Collector { #pages = []; #configuration = Configuration.get(); #markdownCompiler = new MarkdownCompiler(); + #rstCompiler = new RSTCompiler(); #temporaryDirectoryPrefix = '.pd-tmp-'; #temporaryOutputDirectoryPath; #isTemporaryOutputDirectoryPathVirtual; @@ -84,7 +87,7 @@ export default class Collector { ); } - #collectRegularPages() { + #collectMDPages() { const shouldIgnore = anymatch(this.#configuration.ignore.pages); const pages = this.#configuration.directories.content; @@ -97,7 +100,30 @@ export default class Collector { }), AsyncIterable.filter((filePath) => filePath.endsWith('.md')), AsyncIterable.filter((filePath) => !shouldIgnore(filePath)), - AsyncIterable.map((filePath) => this.#tryCreatingPage(filePath, RegularPage, markdownCompiler)), + AsyncIterable.map((filePath) => this.#tryCreatingPage(filePath, MarkdownPage, markdownCompiler)), + AsyncIterable.filter(Boolean), + AsyncIterable.chain((page) => AsyncIterable.from(async function* () { + if (await page.shouldCompile()) { + yield page; + } + })) + ); + } + + #collectRSTPages() { + const shouldIgnore = anymatch(this.#configuration.ignore.pages); + + const pages = this.#configuration.directories.content; + const rstCompiler = this.#rstCompiler; + + return pipeWith(AsyncIterable.from(async function* () { + await rstCompiler.initialise(); + + yield* pipeWith(pages, resolve, walkDirectory, ({ files }) => files); + }), + AsyncIterable.filter((filePath) => filePath.endsWith('.rst')), + AsyncIterable.filter((filePath) => !shouldIgnore(filePath)), + AsyncIterable.map((filePath) => this.#tryCreatingPage(filePath, RSTPage, rstCompiler)), AsyncIterable.filter(Boolean), AsyncIterable.chain((page) => AsyncIterable.from(async function* () { if (await page.shouldCompile()) { @@ -129,7 +155,8 @@ export default class Collector { const layoutsUsedInPages = new Set(); const pages = await pipeWith( - this.#collectRegularPages(), + this.#collectMDPages(), + AsyncIterable.concat(this.#collectRSTPages()), AsyncIterable.concat(this.#collectApiDocs()), AsyncIterable.fold([], (pages, page) => { pages.push(page); diff --git a/lib/regular-page.js b/lib/markdown-page.js similarity index 91% rename from lib/regular-page.js rename to lib/markdown-page.js index 43b7860..66b024c 100644 --- a/lib/regular-page.js +++ b/lib/markdown-page.js @@ -7,7 +7,7 @@ import Page from './page.js'; import Logger from './logger.js'; import Configuration from './configuration.js'; -export default class RegularPage extends Page { +export default class MarkdownPage extends Page { static #findLayout(directoryPath, contentPageName, configuration) { const absoluteLayoutsPath = resolve(configuration.directories.layouts); @@ -41,7 +41,7 @@ export default class RegularPage extends Page { const rootPagesDirectoryPath = resolve(configuration.directories.content); const outputFilePath = Page.resolveOutputFilePath(contentFilePath, rootPagesDirectoryPath, temporaryOutputDirectoryName); - const layoutFilePath = RegularPage.#findLayout(dirname(contentFilePath.replace(rootPagesDirectoryPath, resolve(configuration.directories.layouts))), contentPageName, configuration); + const layoutFilePath = MarkdownPage.#findLayout(dirname(contentFilePath.replace(rootPagesDirectoryPath, resolve(configuration.directories.layouts))), contentPageName, configuration); super(layoutFilePath, outputFilePath, temporaryOutputDirectoryName); diff --git a/lib/rst-compiler.js b/lib/rst-compiler.js new file mode 100644 index 0000000..c5e5849 --- /dev/null +++ b/lib/rst-compiler.js @@ -0,0 +1,67 @@ +import fm from 'front-matter'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import child_process from 'node:child_process'; +import Configuration from './configuration.js'; + +const INTERPOLATION_RE = /{{([^}]+)}}/g; + + +class RSTToHTML { + async parse(rstText){ + + // Needs pandoc installed + // https://github.com/jgm/pandoc/releases + // pandoc index.rst -f rst -t html -o index.html + + const rstFilePath = path.join(os.tmpdir(), "file.rst") + fs.writeFileSync(rstFilePath, rstText, 'utf-8') + const htmlFilePath = path.join(os.tmpdir(), "file.html") + const command = `pandoc ${rstFilePath} -f rst -t html -o ${htmlFilePath}` + child_process.exec(command, (error, out, err) => { return }) + const htmlContent = fs.readFileSync(htmlFilePath, 'utf-8') + fs.unlinkSync(htmlFilePath) + + if (!htmlContent) { + throw Error("Could not compile rst file to html") + } + + return htmlContent + + } +} + + +export default class RSTCompiler { + #compiler; + + async initialise() { + if (this.#compiler) { + return; + } + + this.#compiler = new RSTToHTML(); + + } + + async compile(content) { + const { attributes, body } = fm(content); + + const html = await this.#compiler.parse(body); + + const configuration = Configuration.get(); + + return [attributes, html.replace(INTERPOLATION_RE, (_, variableName) => { + const trimmedVariableName = variableName.trim(); + + if (trimmedVariableName.startsWith('appSettings')) { + const properties = trimmedVariableName.split('.').slice(1); + + return properties.reduce((target, property) => target[property.trim()], configuration.appSettings); + } + + return attributes[trimmedVariableName]; + })]; + } +} diff --git a/lib/rst-page.js b/lib/rst-page.js new file mode 100644 index 0000000..8bef964 --- /dev/null +++ b/lib/rst-page.js @@ -0,0 +1,85 @@ +import { inspect } from 'node:util'; +import { readFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { basename, dirname, extname, join, resolve } from 'node:path'; + +import Page from './page.js'; +import Logger from './logger.js'; +import Configuration from './configuration.js'; + +export default class RSTPage extends Page { + static #findLayout(directoryPath, contentPageName, configuration) { + const absoluteLayoutsPath = resolve(configuration.directories.layouts); + + if (!directoryPath.includes(absoluteLayoutsPath)) { + return; + } + + const namedLayoutPath = join(directoryPath, contentPageName + '.ejs'); + const sharedLayoutPath = join(directoryPath, 'index.ejs'); + + if (existsSync(namedLayoutPath)) { + return namedLayoutPath; + } + + if (existsSync(sharedLayoutPath)) { + return sharedLayoutPath; + } + + if (directoryPath !== absoluteLayoutsPath) { + return this.#findLayout(join(directoryPath, '..'), contentPageName, configuration); + } + } + + #contentFilePath; + #rstCompiler; + + constructor(contentFilePath, temporaryOutputDirectoryName, rstCompiler) { + const configuration = Configuration.get(); + + const contentPageName = basename(contentFilePath, extname(contentFilePath)); + const rootPagesDirectoryPath = resolve(configuration.directories.content); + + const outputFilePath = Page.resolveOutputFilePath(contentFilePath, rootPagesDirectoryPath, temporaryOutputDirectoryName); + const layoutFilePath = RSTPage.#findLayout(dirname(contentFilePath.replace(rootPagesDirectoryPath, resolve(configuration.directories.layouts))), contentPageName, configuration); + + super(layoutFilePath, outputFilePath, temporaryOutputDirectoryName); + + this.#contentFilePath = contentFilePath; + this.#rstCompiler = rstCompiler; + } + + async #compileContent(allowDrafts) { + const content = await readFile(this.#contentFilePath, 'utf8'); + + try { + const result = await this.#rstCompiler.compile(content); + + if (result[0].draft && !allowDrafts) { + return; + } + + return result; + } catch (error) { + Logger.log((typography) => ` + Cannot compile the ${typography.bold(this.#contentFilePath)} because of the following error: + ${inspect(error, { compact: false, colors: true })} + `, Logger.ErrorLevel); + } + } + + async compile(pages, allowDrafts) { + const compiledContentAndFrontMatter = await this.#compileContent(allowDrafts); + + if (!compiledContentAndFrontMatter) { + // All relative errors have been displayed. Just abort writing. + return; + } + + const [frontMatter, content] = compiledContentAndFrontMatter; + + return this.compileLayout(pages, { + ...frontMatter, content + }); + } +} diff --git a/package-lock.json b/package-lock.json index 230374a..ce08129 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14877,4 +14877,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 6ec0fbc..0757834 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "globals": "^13.24.0", "mock-fs": "^5.2.0", "mock-require": "^3.0.3", - "sinon": "^15.0.1", + "sinon": "^17.0.1", "tree-kill": "^1.2.2" }, "dependencies": {