Skip to content

Commit 37faeb2

Browse files
committed
Add basic support for serving markdown files
1 parent f693845 commit 37faeb2

8 files changed

Lines changed: 169 additions & 55 deletions

File tree

cli/ascaid-gfm-to-confluence.js

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
1-
import { Argument, Option, program } from "commander";
21
import path from "node:path";
32
import fs from "node:fs";
43
import assert from "node:assert";
4+
import { Argument, Option, program } from "commander";
55

6-
import { pandocConvert } from "../index.js";
7-
import { readVersion } from "../index.js";
8-
import { ConfluenceClient } from "../index.js";
9-
10-
const MD_TITLE_REGEX = /^#+\s+(.*)/;
11-
12-
const isNotNullOrEmptyString = (string_) => {
13-
return (
14-
string_ != undefined && typeof string_ === "string" && string_.trim() !== ""
15-
);
16-
};
6+
import {
7+
ConfluenceClient,
8+
getTitleFromMarkdown,
9+
isNotNullOrEmptyString,
10+
mdConvert,
11+
normalizeSupportedExtnames,
12+
readVersion,
13+
} from "../index.js";
1714

1815
const createPageTree = async (title, filePath) => {
1916
const dirContents = await fs.promises.readdir(filePath);
2017
const files = dirContents.map((file) => ({
2118
name: file,
22-
extension: path.extname(file),
19+
normalizedExtension: normalizeSupportedExtnames(path.extname(file)),
2320
path: `${filePath}/${file}`,
2421
isDirectory: fs.lstatSync(`${filePath}/${file}`).isDirectory(),
2522
}));
@@ -30,22 +27,12 @@ const createPageTree = async (title, filePath) => {
3027
if (file.isDirectory) {
3128
children.push(await createPageTree(file.name, file.path));
3229
} else {
33-
if (file.extension.toLowerCase() !== ".md") continue;
30+
if (file.normalizedExtension !== ".md") continue;
3431
const contents = fs.readFileSync(file.path, { encoding: "utf8" });
3532

36-
let title = file.name.slice(
37-
0,
38-
Math.max(0, file.name.length - file.extension.length)
39-
);
40-
const firstLine = contents
41-
.split(/\n\r?/)
42-
.find((line) => MD_TITLE_REGEX.test(line.trim()));
43-
if (firstLine != undefined) {
44-
title = firstLine.match(MD_TITLE_REGEX)[1].trim();
45-
}
46-
const body = await pandocConvert(contents, "gfm", "html", [
47-
"--wrap=none",
48-
]);
33+
const title =
34+
(await getTitleFromMarkdown(contents)) ?? path.parse(file.name).name;
35+
const body = await mdConvert(contents);
4936
children.push({
5037
title,
5138
body,

cli/ascaid-serve.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ program
1919
.addOption(configOption)
2020
.addOption(attributeOption)
2121
.description("Start an AsciiDoc server")
22-
.action(async (rootDir, { config, attribute }) => {
23-
const { extensions, asciidoctorOptions } = await readConfig(
24-
config,
25-
attribute
26-
);
27-
await registerExtensions(extensions ?? [], path.resolve("."));
22+
.action(
23+
async (
24+
rootDir,
25+
{ config: configFilePath, attribute: attributeOverrideKvs }
26+
) => {
27+
const config = await readConfig(configFilePath, attributeOverrideKvs);
2828

29-
await startAsciidocServer(rootDir, asciidoctorOptions);
30-
});
29+
await registerExtensions(config.extensions ?? [], path.resolve("."));
30+
await startAsciidocServer(rootDir, config);
31+
}
32+
);
3133

3234
await program.parseAsync(process.argv);

cli/ascaid.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { program } from "commander";
2-
import { readVersion } from "../index.js";
3-
import { checkPandoc } from "../index.js";
2+
import { readVersion, checkPandoc } from "../index.js";
43

54
const version = await readVersion();
65

index.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
export { checkPandoc, pandocConvert } from "./lib/pandoc-convert.js";
2+
export { mdConvert, getTitleFromMarkdown } from "./lib/md-convert.js";
23
export { adocConvert } from "./lib/adoc-convert.js";
3-
export { invokeInDir, readConfig, readVersion } from "./lib/utils.js";
4+
export {
5+
invokeInDir,
6+
readConfig,
7+
readVersion,
8+
normalizeSupportedExtnames,
9+
isNotNullOrEmptyString,
10+
} from "./lib/utils.js";
411
export { registerExtensions } from "./lib/asciidoctor.js";
512
export { ConfluenceClient } from "./lib/confluence-client.js";
613
export {

lib/adoc-server.js

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ import path from "node:path";
44
import http from "node:http";
55
import browserSync from "browser-sync";
66

7-
import { fileExists, invokeInDir } from "./utils.js";
7+
import {
8+
asciidocExtensions,
9+
fileExists,
10+
invokeInDir,
11+
markdownExtensions,
12+
normalizeSupportedExtnames,
13+
} from "./utils.js";
814
import { adocConvert } from "./adoc-convert.js";
15+
import { getTitleFromMarkdown, mdConvert } from "./md-convert.js";
916

10-
export const createAsciidocMiddleware = (rootDir, asciidoctorOptions = {}) => {
17+
export const createAsciidocMiddleware = (rootDir, config = {}) => {
1118
const absoluteRootDir = path.resolve(rootDir);
1219

1320
return async (request, res, next) => {
@@ -21,20 +28,37 @@ export const createAsciidocMiddleware = (rootDir, asciidoctorOptions = {}) => {
2128
return res.end(http.STATUS_CODES[res.statusCode]);
2229
}
2330

24-
if (/\.(adoc|asciidoc|acs)$/i.test(url.pathname)) {
25-
const adocPath = path.join(absoluteRootDir, url.pathname);
26-
const exists = await fileExists(adocPath);
27-
if (!exists || !adocPath.startsWith(absoluteRootDir)) {
31+
const extname = normalizeSupportedExtnames(path.extname(url.pathname));
32+
33+
if ([".md", ".adoc"].includes(extname)) {
34+
const filePath = path.join(absoluteRootDir, url.pathname);
35+
const exists = await fileExists(filePath);
36+
if (!exists || !filePath.startsWith(absoluteRootDir)) {
2837
res.statusCode = 404;
2938

3039
return res.end(http.STATUS_CODES[res.statusCode]);
3140
}
3241

33-
const adoc = await fs.readFile(adocPath, { encoding: "utf8" });
34-
let html = await invokeInDir(path.dirname(adocPath), () => {
35-
return adocConvert(adoc, asciidoctorOptions);
42+
const contents = await fs.readFile(filePath, { encoding: "utf8" });
43+
let html = await invokeInDir(path.dirname(filePath), async () => {
44+
switch (extname) {
45+
case ".adoc": {
46+
return adocConvert(contents, config.asciidoctorOptions);
47+
}
48+
case ".md": {
49+
const title =
50+
(await getTitleFromMarkdown(contents)) ?? "Untitled";
51+
return mdConvert(contents, config.markdownOptions).then(
52+
(body) =>
53+
`<!DOCTYPE html><html><head><title>${title}</title></head><body>${body}</body></html>`
54+
);
55+
}
56+
default: {
57+
throw new Error(`Unsupported extension: ${extname}`);
58+
}
59+
}
3660
});
37-
res.setHeader("Content-Type", "text/html");
61+
res.setHeader("Content-Type", "text/html; charset=utf-8");
3862
html = html.replace(
3963
/<\/head>/,
4064
`<script async src="//${request.headers.host}/browser-sync/browser-sync-client.js"></script></head>`
@@ -52,17 +76,14 @@ export const createAsciidocMiddleware = (rootDir, asciidoctorOptions = {}) => {
5276
};
5377
};
5478

55-
export const startAsciidocServer = async (
56-
rootDir = ".",
57-
asciidoctorOptions = {}
58-
) => {
79+
export const startAsciidocServer = async (rootDir = ".", config = {}) => {
5980
const bs = browserSync.create();
6081

6182
bs.init({
62-
files: "**/*.{adoc,asciidoc,acs}",
83+
files: `**/*.{${[...asciidocExtensions, ...markdownExtensions].join(",")}`,
6384
server: rootDir,
64-
injectFileTypes: ["adoc", "asciidoc", "acs"],
65-
middleware: [createAsciidocMiddleware(rootDir, asciidoctorOptions)],
85+
injectFileTypes: [...asciidocExtensions, ...markdownExtensions],
86+
middleware: [createAsciidocMiddleware(rootDir, config)],
6687
directory: true,
6788
open: false,
6889
ui: false,

lib/md-convert.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { pandocConvert } from "./pandoc-convert.js";
2+
3+
const MD_TITLE_REGEX = /^#+\s+(.*)/;
4+
5+
const defaultMarkdownOptions = {
6+
pandocReadFormat: "gfm",
7+
pandocArguments: ["--wrap=none"],
8+
};
9+
10+
export const getTitleFromMarkdown = (contents) => {
11+
const firstHeading = contents
12+
.split(/\n\r?/)
13+
.find((line) => MD_TITLE_REGEX.test(line.trim()));
14+
15+
if (firstHeading != undefined) {
16+
return firstHeading.match(MD_TITLE_REGEX)[1].trim();
17+
}
18+
19+
return;
20+
};
21+
22+
export const mdConvert = async (contents, markdownOptions = {}) => {
23+
const mergedMarkdownOptions = {
24+
...defaultMarkdownOptions,
25+
...markdownOptions,
26+
};
27+
28+
return pandocConvert(
29+
contents,
30+
mergedMarkdownOptions.pandocReadFormat,
31+
"html",
32+
mergedMarkdownOptions.pandocArguments
33+
);
34+
};

lib/md-convert.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { mdConvert } from "./md-convert.js";
2+
3+
describe("mdConvert", () => {
4+
describe("when config is valid", () => {
5+
it("should convert input to the output", async () => {
6+
const html = await mdConvert("# Hello");
7+
expect(html).toBe('<h1 id="hello">Hello</h1>\n');
8+
});
9+
});
10+
11+
describe("when config is not valid", () => {
12+
it("should throw an error", async () => {
13+
const error = await mdConvert("# Hello", {
14+
pandocReadFormat: "non-existent",
15+
}).catch((error) => error);
16+
expect(error).toBeInstanceOf(Error);
17+
});
18+
});
19+
});

lib/utils.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,51 @@ import stripJsonComments from "strip-json-comments";
55

66
const __dirname = path.dirname(fileURLToPath(import.meta.url));
77

8+
export const markdownExtensions = [
9+
"md",
10+
"markdown",
11+
"mdown",
12+
"mkdn",
13+
"mkd",
14+
"mdwn",
15+
"mkdown",
16+
"ron",
17+
];
18+
19+
export const asciidocExtensions = ["adoc", "asciidoc", "acs"];
20+
21+
/**
22+
* Normalize supported extensions
23+
*
24+
* @param {string} extname
25+
* @return {".md" | ".adoc" | undefined} ".md" for markdown extensions, ".adoc" for asciidoc extensions. Otherwise, undefined
26+
*/
27+
export const normalizeSupportedExtnames = (extname) => {
28+
if (!extname.startsWith(".")) {
29+
return;
30+
}
31+
32+
const extnameWithoutLeadingDot = extname.slice(1).toLowerCase();
33+
34+
if (markdownExtensions.includes(extnameWithoutLeadingDot)) {
35+
return ".md";
36+
}
37+
38+
if (asciidocExtensions.includes(extnameWithoutLeadingDot)) {
39+
return ".adoc";
40+
}
41+
42+
return;
43+
};
44+
45+
export const isNotNullOrEmptyString = (maybeString) => {
46+
return (
47+
maybeString != undefined &&
48+
typeof maybeString === "string" &&
49+
maybeString.trim() !== ""
50+
);
51+
};
52+
853
export const fileExists = async (path) =>
954
!!(await fs.promises.stat(path).catch(() => false));
1055

0 commit comments

Comments
 (0)