Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support serving markdown #71

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
18
6 changes: 3 additions & 3 deletions cli/ascaid-adoc-to-gfm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);

Expand Down Expand Up @@ -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);
41 changes: 14 additions & 27 deletions cli/ascaid-gfm-to-confluence.js
Original file line number Diff line number Diff line change
@@ -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(),
}));
Expand All @@ -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,
Expand Down
18 changes: 10 additions & 8 deletions cli/ascaid-serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ program
.addOption(configOption)
.addOption(attributeOption)
.description("Start an AsciiDoc server")
.action(async (rootDir, { config, attribute }) => {
const { extensions, asciidoctorOptions: adoctorOptions } = 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, adoctorOptions);
});
await registerExtensions(config.extensions ?? [], path.resolve("."));
await startAsciidocServer(rootDir, config);
}
);

await program.parseAsync(process.argv);
3 changes: 1 addition & 2 deletions cli/ascaid.js
Original file line number Diff line number Diff line change
@@ -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();

Expand Down
8 changes: 7 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
13 changes: 8 additions & 5 deletions lib/adoc-convert.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
adoctor,
asciidoctor,
ASCIIDOCTOR_MESSAGE_SEVERITY,
memoryLogger,
} from "./asciidoctor.js";

const defaultAdoctorOptions = {
const defaultAsciidoctorOptions = {
safe: "server",
doctype: "book",
standalone: true,
Expand All @@ -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) {
Expand Down
55 changes: 38 additions & 17 deletions lib/adoc-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, adoctorOptions = {}) => {
export const createAsciidocMiddleware = (rootDir, config = {}) => {
const absoluteRootDir = path.resolve(rootDir);

return async (request, res, next) => {
Expand All @@ -21,20 +28,37 @@ export const createAsciidocMiddleware = (rootDir, adoctorOptions = {}) => {
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, adoctorOptions);
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) =>
`<!DOCTYPE html><html><head><title>${title}</title></head><body>${body}</body></html>`
);
}
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>/,
`<script async src="//${request.headers.host}/browser-sync/browser-sync-client.js"></script></head>`
Expand All @@ -52,17 +76,14 @@ export const createAsciidocMiddleware = (rootDir, adoctorOptions = {}) => {
};
};

export const startAsciidocServer = async (
rootDir = ".",
adoctorOptions = {}
) => {
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, adoctorOptions)],
injectFileTypes: [...asciidocExtensions, ...markdownExtensions],
middleware: [createAsciidocMiddleware(rootDir, config)],
directory: true,
open: false,
ui: false,
Expand Down
10 changes: 5 additions & 5 deletions lib/asciidoctor.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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_)) {
Expand All @@ -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)
);
}
};
34 changes: 34 additions & 0 deletions lib/md-convert.js
Original file line number Diff line number Diff line change
@@ -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
);
};
19 changes: 19 additions & 0 deletions lib/md-convert.test.js
Original file line number Diff line number Diff line change
@@ -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('<h1 id="hello">Hello</h1>\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);
});
});
});
45 changes: 45 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down