From 512a951fe069716df78938519c26d81012d231e0 Mon Sep 17 00:00:00 2001 From: Fardjad Davari Date: Sun, 27 Nov 2022 14:14:00 +0000 Subject: [PATCH 1/3] Add .nvmrc --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..25bf17f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 \ No newline at end of file From 10d2a390244cfff022b85f9d36d6fd02c11769fd Mon Sep 17 00:00:00 2001 From: Fardjad Davari Date: Sun, 27 Nov 2022 14:19:15 +0000 Subject: [PATCH 2/3] Rename adoctor to asciidoctor in asciidoctor.js --- cli/ascaid-adoc-to-gfm.js | 6 +++--- cli/ascaid-serve.js | 4 ++-- lib/adoc-convert.js | 13 ++++++++----- lib/adoc-server.js | 8 ++++---- lib/asciidoctor.js | 10 +++++----- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/cli/ascaid-adoc-to-gfm.js b/cli/ascaid-adoc-to-gfm.js index f89f212..5caa9b2 100644 --- a/cli/ascaid-adoc-to-gfm.js +++ b/cli/ascaid-adoc-to-gfm.js @@ -28,7 +28,7 @@ export const adocToGfm = async (srcDir, outDir, ignore, adoctorOptions) => { const readDir = path.join(srcDir, dirname); const html = await invokeInDir(readDir, () => { - return adocConvert(adoc, adoctorOptions); + return adocConvert(adoc, asciidoctorOptions); }); const gfm = await pandocConvert(html, "html", "gfm", ["--wrap=none"]); @@ -62,13 +62,13 @@ program "Recursively convert AsciiDoc files in a directory to GitHub flavored markdown" ) .action(async (srcDir, outDir, { ignore, config, attribute }) => { - const { extensions, asciidoctorOptions: adoctorOptions } = await readConfig( + const { extensions, asciidoctorOptions } = await readConfig( config, attribute ); await registerExtensions(extensions ?? [], path.resolve(".")); - await adocToGfm(srcDir, outDir, ignore, adoctorOptions); + await adocToGfm(srcDir, outDir, ignore, asciidoctorOptions); }); await program.parseAsync(process.argv); diff --git a/cli/ascaid-serve.js b/cli/ascaid-serve.js index 3617048..55748b3 100644 --- a/cli/ascaid-serve.js +++ b/cli/ascaid-serve.js @@ -20,13 +20,13 @@ program .addOption(attributeOption) .description("Start an AsciiDoc server") .action(async (rootDir, { config, attribute }) => { - const { extensions, asciidoctorOptions: adoctorOptions } = await readConfig( + const { extensions, asciidoctorOptions } = await readConfig( config, attribute ); await registerExtensions(extensions ?? [], path.resolve(".")); - await startAsciidocServer(rootDir, adoctorOptions); + await startAsciidocServer(rootDir, asciidoctorOptions); }); await program.parseAsync(process.argv); diff --git a/lib/adoc-convert.js b/lib/adoc-convert.js index ae7ddd8..77e1667 100644 --- a/lib/adoc-convert.js +++ b/lib/adoc-convert.js @@ -1,10 +1,10 @@ import { - adoctor, + asciidoctor, ASCIIDOCTOR_MESSAGE_SEVERITY, memoryLogger, } from "./asciidoctor.js"; -const defaultAdoctorOptions = { +const defaultAsciidoctorOptions = { safe: "server", doctype: "book", standalone: true, @@ -17,9 +17,12 @@ const getNumericAsciidoctorMessageSeverity = (message) => { ); }; -export const adocConvert = async (adoc, adoctorOptions = {}) => { - const mergedAdoctorOptions = { ...defaultAdoctorOptions, ...adoctorOptions }; - const html = adoctor.convert(adoc, mergedAdoctorOptions); +export const adocConvert = async (adoc, asciidoctorOptions = {}) => { + const mergedAsciidoctorOptions = { + ...defaultAsciidoctorOptions, + ...asciidoctorOptions, + }; + const html = asciidoctor.convert(adoc, mergedAsciidoctorOptions); const messages = memoryLogger.getMessages(); for (const message of messages) { diff --git a/lib/adoc-server.js b/lib/adoc-server.js index 0c1a730..e1e58bc 100644 --- a/lib/adoc-server.js +++ b/lib/adoc-server.js @@ -7,7 +7,7 @@ import browserSync from "browser-sync"; import { fileExists, invokeInDir } from "./utils.js"; import { adocConvert } from "./adoc-convert.js"; -export const createAsciidocMiddleware = (rootDir, adoctorOptions = {}) => { +export const createAsciidocMiddleware = (rootDir, asciidoctorOptions = {}) => { const absoluteRootDir = path.resolve(rootDir); return async (request, res, next) => { @@ -32,7 +32,7 @@ export const createAsciidocMiddleware = (rootDir, adoctorOptions = {}) => { const adoc = await fs.readFile(adocPath, { encoding: "utf8" }); let html = await invokeInDir(path.dirname(adocPath), () => { - return adocConvert(adoc, adoctorOptions); + return adocConvert(adoc, asciidoctorOptions); }); res.setHeader("Content-Type", "text/html"); html = html.replace( @@ -54,7 +54,7 @@ export const createAsciidocMiddleware = (rootDir, adoctorOptions = {}) => { export const startAsciidocServer = async ( rootDir = ".", - adoctorOptions = {} + asciidoctorOptions = {} ) => { const bs = browserSync.create(); @@ -62,7 +62,7 @@ export const startAsciidocServer = async ( files: "**/*.{adoc,asciidoc,acs}", server: rootDir, injectFileTypes: ["adoc", "asciidoc", "acs"], - middleware: [createAsciidocMiddleware(rootDir, adoctorOptions)], + middleware: [createAsciidocMiddleware(rootDir, asciidoctorOptions)], directory: true, open: false, ui: false, diff --git a/lib/asciidoctor.js b/lib/asciidoctor.js index 06f77a7..42e41e0 100644 --- a/lib/asciidoctor.js +++ b/lib/asciidoctor.js @@ -1,5 +1,5 @@ import path from "node:path"; -import asciidoctor from "@asciidoctor/core"; +import createAsciidoctor from "@asciidoctor/core"; export const ASCIIDOCTOR_MESSAGE_SEVERITY = { DEBUG: 0, @@ -9,9 +9,9 @@ export const ASCIIDOCTOR_MESSAGE_SEVERITY = { FATAL: 4, }; -export const adoctor = asciidoctor(); -export const memoryLogger = adoctor.MemoryLogger.$new(); -adoctor.LoggerManager.setLogger(memoryLogger); +export const asciidoctor = createAsciidoctor(); +export const memoryLogger = asciidoctor.MemoryLogger.$new(); +asciidoctor.LoggerManager.setLogger(memoryLogger); const shouldResolveAsPath = (string_) => { if (path.isAbsolute(string_) || /^([A-Za-z]:)/.test(string_)) { @@ -28,7 +28,7 @@ export const registerExtensions = async (extensions, dir) => { : import(extensionPath)); await Promise.resolve( - (module.register ?? module.default.register)(adoctor.Extensions) + (module.register ?? module.default.register)(asciidoctor.Extensions) ); } }; From ec4d2ac7d7c0771e69626cbb8b8e4c4041d17849 Mon Sep 17 00:00:00 2001 From: Fardjad Davari Date: Sun, 27 Nov 2022 18:45:06 +0000 Subject: [PATCH 3/3] Add basic support for serving markdown files --- cli/ascaid-gfm-to-confluence.js | 41 +++++++++--------------- cli/ascaid-serve.js | 18 ++++++----- cli/ascaid.js | 3 +- index.js | 8 ++++- lib/adoc-server.js | 55 +++++++++++++++++++++++---------- lib/md-convert.js | 34 ++++++++++++++++++++ lib/md-convert.test.js | 19 ++++++++++++ lib/utils.js | 45 +++++++++++++++++++++++++++ 8 files changed, 168 insertions(+), 55 deletions(-) create mode 100644 lib/md-convert.js create mode 100644 lib/md-convert.test.js diff --git a/cli/ascaid-gfm-to-confluence.js b/cli/ascaid-gfm-to-confluence.js index 1cd01b7..637ef93 100644 --- a/cli/ascaid-gfm-to-confluence.js +++ b/cli/ascaid-gfm-to-confluence.js @@ -1,25 +1,22 @@ -import { Argument, Option, program } from "commander"; import path from "node:path"; import fs from "node:fs"; import assert from "node:assert"; +import { Argument, Option, program } from "commander"; -import { pandocConvert } from "../index.js"; -import { readVersion } from "../index.js"; -import { ConfluenceClient } from "../index.js"; - -const MD_TITLE_REGEX = /^#+\s+(.*)/; - -const isNotNullOrEmptyString = (string_) => { - return ( - string_ != undefined && typeof string_ === "string" && string_.trim() !== "" - ); -}; +import { + ConfluenceClient, + getTitleFromMarkdown, + isNotNullOrEmptyString, + mdConvert, + normalizeSupportedExtnames, + readVersion, +} from "../index.js"; const createPageTree = async (title, filePath) => { const dirContents = await fs.promises.readdir(filePath); const files = dirContents.map((file) => ({ name: file, - extension: path.extname(file), + normalizedExtension: normalizeSupportedExtnames(path.extname(file)), path: `${filePath}/${file}`, isDirectory: fs.lstatSync(`${filePath}/${file}`).isDirectory(), })); @@ -30,22 +27,12 @@ const createPageTree = async (title, filePath) => { if (file.isDirectory) { children.push(await createPageTree(file.name, file.path)); } else { - if (file.extension.toLowerCase() !== ".md") continue; + if (file.normalizedExtension !== ".md") continue; const contents = fs.readFileSync(file.path, { encoding: "utf8" }); - let title = file.name.slice( - 0, - Math.max(0, file.name.length - file.extension.length) - ); - const firstLine = contents - .split(/\n\r?/) - .find((line) => MD_TITLE_REGEX.test(line.trim())); - if (firstLine != undefined) { - title = firstLine.match(MD_TITLE_REGEX)[1].trim(); - } - const body = await pandocConvert(contents, "gfm", "html", [ - "--wrap=none", - ]); + const title = + (await getTitleFromMarkdown(contents)) ?? path.parse(file.name).name; + const body = await mdConvert(contents); children.push({ title, body, diff --git a/cli/ascaid-serve.js b/cli/ascaid-serve.js index 55748b3..7d19f29 100644 --- a/cli/ascaid-serve.js +++ b/cli/ascaid-serve.js @@ -19,14 +19,16 @@ program .addOption(configOption) .addOption(attributeOption) .description("Start an AsciiDoc server") - .action(async (rootDir, { config, attribute }) => { - const { extensions, asciidoctorOptions } = await readConfig( - config, - attribute - ); - await registerExtensions(extensions ?? [], path.resolve(".")); + .action( + async ( + rootDir, + { config: configFilePath, attribute: attributeOverrideKvs } + ) => { + const config = await readConfig(configFilePath, attributeOverrideKvs); - await startAsciidocServer(rootDir, asciidoctorOptions); - }); + await registerExtensions(config.extensions ?? [], path.resolve(".")); + await startAsciidocServer(rootDir, config); + } + ); await program.parseAsync(process.argv); diff --git a/cli/ascaid.js b/cli/ascaid.js index 3448d4a..146c55c 100755 --- a/cli/ascaid.js +++ b/cli/ascaid.js @@ -1,6 +1,5 @@ import { program } from "commander"; -import { readVersion } from "../index.js"; -import { checkPandoc } from "../index.js"; +import { readVersion, checkPandoc } from "../index.js"; const version = await readVersion(); diff --git a/index.js b/index.js index 3c9efec..5531b83 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,12 @@ export { checkPandoc, pandocConvert } from "./lib/pandoc-convert.js"; +export { mdConvert, getTitleFromMarkdown } from "./lib/md-convert.js"; export { adocConvert } from "./lib/adoc-convert.js"; -export { invokeInDir, readVersion } from "./lib/utils.js"; +export { + invokeInDir, + readVersion, + normalizeSupportedExtnames, + isNotNullOrEmptyString, +} from "./lib/utils.js"; export { readConfig } from "./lib/config.js"; export { registerExtensions } from "./lib/asciidoctor.js"; export { ConfluenceClient } from "./lib/confluence-client.js"; diff --git a/lib/adoc-server.js b/lib/adoc-server.js index e1e58bc..6df3b9e 100644 --- a/lib/adoc-server.js +++ b/lib/adoc-server.js @@ -4,10 +4,17 @@ import path from "node:path"; import http from "node:http"; import browserSync from "browser-sync"; -import { fileExists, invokeInDir } from "./utils.js"; +import { + asciidocExtensions, + fileExists, + invokeInDir, + markdownExtensions, + normalizeSupportedExtnames, +} from "./utils.js"; import { adocConvert } from "./adoc-convert.js"; +import { getTitleFromMarkdown, mdConvert } from "./md-convert.js"; -export const createAsciidocMiddleware = (rootDir, asciidoctorOptions = {}) => { +export const createAsciidocMiddleware = (rootDir, config = {}) => { const absoluteRootDir = path.resolve(rootDir); return async (request, res, next) => { @@ -21,20 +28,37 @@ export const createAsciidocMiddleware = (rootDir, asciidoctorOptions = {}) => { return res.end(http.STATUS_CODES[res.statusCode]); } - if (/\.(adoc|asciidoc|acs)$/i.test(url.pathname)) { - const adocPath = path.join(absoluteRootDir, url.pathname); - const exists = await fileExists(adocPath); - if (!exists || !adocPath.startsWith(absoluteRootDir)) { + const extname = normalizeSupportedExtnames(path.extname(url.pathname)); + + if ([".md", ".adoc"].includes(extname)) { + const filePath = path.join(absoluteRootDir, url.pathname); + const exists = await fileExists(filePath); + if (!exists || !filePath.startsWith(absoluteRootDir)) { res.statusCode = 404; return res.end(http.STATUS_CODES[res.statusCode]); } - const adoc = await fs.readFile(adocPath, { encoding: "utf8" }); - let html = await invokeInDir(path.dirname(adocPath), () => { - return adocConvert(adoc, asciidoctorOptions); + const contents = await fs.readFile(filePath, { encoding: "utf8" }); + let html = await invokeInDir(path.dirname(filePath), async () => { + switch (extname) { + case ".adoc": { + return adocConvert(contents, config.asciidoctorOptions); + } + case ".md": { + const title = + (await getTitleFromMarkdown(contents)) ?? "Untitled"; + return mdConvert(contents, config.markdownOptions).then( + (body) => + `${title}${body}` + ); + } + default: { + throw new Error(`Unsupported extension: ${extname}`); + } + } }); - res.setHeader("Content-Type", "text/html"); + res.setHeader("Content-Type", "text/html; charset=utf-8"); html = html.replace( /<\/head>/, `` @@ -52,17 +76,14 @@ export const createAsciidocMiddleware = (rootDir, asciidoctorOptions = {}) => { }; }; -export const startAsciidocServer = async ( - rootDir = ".", - asciidoctorOptions = {} -) => { +export const startAsciidocServer = async (rootDir = ".", config = {}) => { const bs = browserSync.create(); bs.init({ - files: "**/*.{adoc,asciidoc,acs}", + files: `**/*.{${[...asciidocExtensions, ...markdownExtensions].join(",")}`, server: rootDir, - injectFileTypes: ["adoc", "asciidoc", "acs"], - middleware: [createAsciidocMiddleware(rootDir, asciidoctorOptions)], + injectFileTypes: [...asciidocExtensions, ...markdownExtensions], + middleware: [createAsciidocMiddleware(rootDir, config)], directory: true, open: false, ui: false, diff --git a/lib/md-convert.js b/lib/md-convert.js new file mode 100644 index 0000000..e64f951 --- /dev/null +++ b/lib/md-convert.js @@ -0,0 +1,34 @@ +import { pandocConvert } from "./pandoc-convert.js"; + +const MD_TITLE_REGEX = /^#+\s+(.*)/; + +const defaultMarkdownOptions = { + pandocReadFormat: "gfm", + pandocArguments: ["--wrap=none"], +}; + +export const getTitleFromMarkdown = (contents) => { + const firstHeading = contents + .split(/\n\r?/) + .find((line) => MD_TITLE_REGEX.test(line.trim())); + + if (firstHeading != undefined) { + return firstHeading.match(MD_TITLE_REGEX)[1].trim(); + } + + return; +}; + +export const mdConvert = async (contents, markdownOptions = {}) => { + const mergedMarkdownOptions = { + ...defaultMarkdownOptions, + ...markdownOptions, + }; + + return pandocConvert( + contents, + mergedMarkdownOptions.pandocReadFormat, + "html", + mergedMarkdownOptions.pandocArguments + ); +}; diff --git a/lib/md-convert.test.js b/lib/md-convert.test.js new file mode 100644 index 0000000..f7ae4c5 --- /dev/null +++ b/lib/md-convert.test.js @@ -0,0 +1,19 @@ +import { mdConvert } from "./md-convert.js"; + +describe("mdConvert", () => { + describe("when config is valid", () => { + it("should convert input to the output", async () => { + const html = await mdConvert("# Hello"); + expect(html).toBe('

Hello

\n'); + }); + }); + + describe("when config is not valid", () => { + it("should throw an error", async () => { + const error = await mdConvert("# Hello", { + pandocReadFormat: "non-existent", + }).catch((error) => error); + expect(error).toBeInstanceOf(Error); + }); + }); +}); diff --git a/lib/utils.js b/lib/utils.js index 509117d..6e74466 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,6 +4,51 @@ import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +export const markdownExtensions = [ + "md", + "markdown", + "mdown", + "mkdn", + "mkd", + "mdwn", + "mkdown", + "ron", +]; + +export const asciidocExtensions = ["adoc", "asciidoc", "acs"]; + +/** + * Normalize supported extensions + * + * @param {string} extname + * @return {".md" | ".adoc" | undefined} ".md" for markdown extensions, ".adoc" for asciidoc extensions. Otherwise, undefined + */ +export const normalizeSupportedExtnames = (extname) => { + if (!extname.startsWith(".")) { + return; + } + + const extnameWithoutLeadingDot = extname.slice(1).toLowerCase(); + + if (markdownExtensions.includes(extnameWithoutLeadingDot)) { + return ".md"; + } + + if (asciidocExtensions.includes(extnameWithoutLeadingDot)) { + return ".adoc"; + } + + return; +}; + +export const isNotNullOrEmptyString = (maybeString) => { + return ( + maybeString != undefined && + typeof maybeString === "string" && + maybeString.trim() !== "" + ); +}; + export const fileExists = async (path) => !!(await fs.promises.stat(path).catch(() => false));