Skip to content

centralize md instance as part of the normalized configuration #1034

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

Merged
merged 5 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 7 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type MarkdownIt from "markdown-it";
import {visitMarkdownFiles} from "./files.js";
import {formatIsoDate, formatLocaleDate} from "./format.js";
import {parseMarkdown} from "./markdown.js";
import {mdparser} from "./markdown.js";
import {resolvePath} from "./path.js";
import {resolveTheme} from "./theme.js";

Expand Down Expand Up @@ -53,7 +54,7 @@ export interface Config {
style: null | Style; // defaults to {theme: ["light", "dark"]}
deploy: null | {workspace: string; project: string};
search: boolean; // default to false
markdownIt?: (md: MarkdownIt) => MarkdownIt;
md: MarkdownIt;
}

/**
Expand All @@ -79,12 +80,12 @@ export async function readDefaultConfig(root?: string): Promise<Config> {
return normalizeConfig((await import(pathToFileURL(tsPath).href)).default, root);
}

async function readPages(root: string): Promise<Page[]> {
async function readPages(root: string, md: MarkdownIt): Promise<Page[]> {
const pages: Page[] = [];
for await (const file of visitMarkdownFiles(root)) {
if (file === "index.md" || file === "404.md") continue;
const source = await readFile(join(root, file), "utf8");
const parsed = parseMarkdown(source, {root, path: file});
const parsed = parseMarkdown(source, {root, path: file, md});
if (parsed?.data?.draft) continue;
const name = basename(file, ".md");
const page = {path: join("/", dirname(file), name), name: parsed.title ?? "Untitled"};
Expand Down Expand Up @@ -117,14 +118,14 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro
currentDate
)}">${formatLocaleDate(currentDate)}</a>.`
} = spec;
const {markdownIt} = spec;
root = String(root);
output = String(output);
base = normalizeBase(base);
if (style === null) style = null;
else if (style !== undefined) style = {path: String(style)};
else style = {theme: (theme = normalizeTheme(theme))};
let {title, pages = await readPages(root), pager = true, toc = true} = spec;
const md = mdparser(spec);
let {title, pages = await readPages(root, md), pager = true, toc = true} = spec;
if (title !== undefined) title = String(title);
pages = Array.from(pages, normalizePageOrSection);
sidebar = sidebar === undefined ? pages.length > 0 : Boolean(sidebar);
Expand All @@ -136,7 +137,6 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro
toc = normalizeToc(toc);
deploy = deploy ? {workspace: String(deploy.workspace).replace(/^@+/, ""), project: String(deploy.project)} : null;
search = Boolean(search);
if (markdownIt !== undefined && typeof markdownIt !== "function") throw new Error("markdownIt must be a function");
return {
root,
output,
Expand All @@ -153,7 +153,7 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro
style,
deploy,
search,
markdownIt
md
};
}

Expand Down
30 changes: 19 additions & 11 deletions src/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface ParseContext {
code: MarkdownCode[];
startLine: number;
currentLine: number;
path: string;
}

function uniqueCodeId(context: ParseContext, content: string): string {
Expand Down Expand Up @@ -85,8 +86,9 @@ function getLiveSource(content: string, tag: string, attributes: Record<string,
// console.error(red(`${error.name}: ${warning}`));
// }

function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: string): RenderRule {
function makeFenceRenderer(baseRenderer: RenderRule): RenderRule {
return (tokens, idx, options, context: ParseContext, self) => {
const {path} = context;
const token = tokens[idx];
const {tag, attributes} = parseInfo(token.info);
token.info = tag;
Expand All @@ -97,7 +99,7 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: s
if (source != null) {
const id = uniqueCodeId(context, source);
// TODO const sourceLine = context.startLine + context.currentLine;
const node = parseJavaScript(source, {path: sourcePath});
const node = parseJavaScript(source, {path});
context.code.push({id, node});
html += `<div id="cell-${id}" class="observablehq observablehq--block${
node.expression ? " observablehq--loading" : ""
Expand Down Expand Up @@ -177,7 +179,7 @@ function parsePlaceholder(content: string, replacer: (i: number, j: number) => v

function transformPlaceholderBlock(token) {
const input = token.content;
if (/^\s*<script(\s|>)/.test(input)) return [token]; // ignore <script> elements
if (/^\s*<script[\s>]/.test(input)) return [token]; // ignore <script> elements
const output: any[] = [];
let i = 0;
parsePlaceholder(input, (j, k) => {
Expand Down Expand Up @@ -245,13 +247,14 @@ const transformPlaceholderCore: RuleCore = (state) => {
state.tokens = output;
};

function makePlaceholderRenderer(root: string, sourcePath: string): RenderRule {
function makePlaceholderRenderer(): RenderRule {
return (tokens, idx, options, context: ParseContext) => {
const {path} = context;
const token = tokens[idx];
const id = uniqueCodeId(context, token.content);
try {
// TODO sourceLine: context.startLine + context.currentLine
const node = parseJavaScript(token.content, {path: sourcePath, inline: true});
const node = parseJavaScript(token.content, {path, inline: true});
context.code.push({id, node});
return `<span id="cell-${id}" class="observablehq--loading"></span>`;
} catch (error) {
Expand All @@ -275,23 +278,28 @@ function makeSoftbreakRenderer(baseRenderer: RenderRule): RenderRule {
export interface ParseOptions {
root: string;
path: string;
markdownIt?: Config["markdownIt"];
style?: Config["style"];
md?: MarkdownIt;
}

export function parseMarkdown(input: string, {root, path, markdownIt, style: configStyle}: ParseOptions): MarkdownPage {
const parts = matter(input, {});
export function mdparser({markdownIt}: {markdownIt?: (md: MarkdownIt) => MarkdownIt} = {}): MarkdownIt {
let md = MarkdownIt({html: true, linkify: true});
md.linkify.set({fuzzyLink: false, fuzzyEmail: false});
if (markdownIt !== undefined) md = markdownIt(md);
md.use(MarkdownItAnchor, {permalink: MarkdownItAnchor.permalink.headerLink({class: "observablehq-header-anchor"})});
md.inline.ruler.push("placeholder", transformPlaceholderInline);
md.core.ruler.before("linkify", "placeholder", transformPlaceholderCore);
md.renderer.rules.placeholder = makePlaceholderRenderer(root, path);
md.renderer.rules.fence = makeFenceRenderer(root, md.renderer.rules.fence!, path);
md.renderer.rules.placeholder = makePlaceholderRenderer();
md.renderer.rules.fence = makeFenceRenderer(md.renderer.rules.fence!);
md.renderer.rules.softbreak = makeSoftbreakRenderer(md.renderer.rules.softbreak!);
return md;
}

export function parseMarkdown(input: string, {path, style: configStyle, md}: ParseOptions): MarkdownPage {
const parts = matter(input, {});
const code: MarkdownCode[] = [];
const context: ParseContext = {code, startLine: 0, currentLine: 0};
const context: ParseContext = {code, startLine: 0, currentLine: 0, path};
if (md === undefined) md = mdparser();
const tokens = md.parse(parts.content, context);
const html = md.renderer.render(tokens, md.options, context); // Note: mutates code, assets!
const style = getStylesheet(path, parts.data, configStyle);
Expand Down
2 changes: 1 addition & 1 deletion src/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function searchIndex(config: Config, effects = defaultEffects): Pro
for await (const file of visitMarkdownFiles(root)) {
const path = join(root, file);
const source = await readFile(path, "utf8");
const {html, title, data} = parseMarkdown(source, {root, path: "/" + file.slice(0, -3)});
const {html, title, data} = parseMarkdown(source, {...config, root, path: "/" + file.slice(0, -3)});

// Skip pages that opt-out of indexing, and skip unlisted pages unless
// opted-in. We only log the first case.
Expand Down
15 changes: 9 additions & 6 deletions test/config-test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import assert from "node:assert";
import MarkdownIt from "markdown-it";
import {normalizeConfig as config, mergeToc, readConfig, setCurrentDate} from "../src/config.js";

const root = "test/input/build/config";

describe("readConfig(undefined, root)", () => {
before(() => setCurrentDate(new Date("2024-01-11T01:02:03")));
it("imports the config file at the specified root", async () => {
assert.deepStrictEqual(await readConfig(undefined, "test/input/build/config"), {
const {md, ...config} = await readConfig(undefined, "test/input/build/config");
assert(md instanceof MarkdownIt);
assert.deepStrictEqual(config, {
root: "test/input/build/config",
output: "dist",
base: "/",
Expand All @@ -30,12 +33,13 @@ describe("readConfig(undefined, root)", () => {
workspace: "acme",
project: "bi"
},
search: false,
markdownIt: undefined
search: false
});
});
it("returns the default config if no config file is found", async () => {
assert.deepStrictEqual(await readConfig(undefined, "test/input/build/simple"), {
const {md, ...config} = await readConfig(undefined, "test/input/build/simple");
assert(md instanceof MarkdownIt);
assert.deepStrictEqual(config, {
root: "test/input/build/simple",
output: "dist",
base: "/",
Expand All @@ -51,8 +55,7 @@ describe("readConfig(undefined, root)", () => {
footer:
'Built with <a href="https://observablehq.com/" target="_blank">Observable</a> on <a title="2024-01-11T01:02:03">Jan 11, 2024</a>.',
deploy: null,
search: false,
markdownIt: undefined
search: false
});
});
});
Expand Down