fix: only suppress comments on explicit false / "false" override

Previously the frontmatter check suppressed comments on any falsy
value (0, "", null, false, "false"), which is wider than the
documented API (docs/features/comments.md only shows `comments: false`).

The refactor also removes the `as Record<string, unknown>` cast
dance that predated the plugin importing proper QuartzComponentProps.
fileData.frontmatter and cfg.baseUrl are now typed directly.

Regression tests cover:
- frontmatter.comments=false / "false" -> suppressed (documented)
- frontmatter.comments=true / "true" / undefined -> rendered
- frontmatter.comments=0 / "" / null -> rendered (behavior change)
- data attribute wiring and defaults
- cfg.baseUrl fallback chain

BEHAVIOR CHANGE: sites that relied on the prior permissive disable
logic (e.g. setting `comments: 0` to hide comments) will now see
comments rendered. Update to `comments: false` to restore the intent.
This commit is contained in:
saberzero1 2026-04-16 15:43:08 +02:00
parent c044e071a2
commit d79ee3c4a0
No known key found for this signature in database
6 changed files with 187 additions and 30 deletions

View file

@ -12,12 +12,10 @@ function boolToStringBool(b) {
}
var Comments_default = ((opts) => {
const Comments = ({ displayClass, fileData, cfg }) => {
const fd = fileData;
const disableComment = typeof fd.frontmatter?.comments !== "undefined" && (!fd.frontmatter?.comments || fd.frontmatter?.comments === "false");
if (disableComment) {
const commentsOverride = fileData.frontmatter?.comments;
if (commentsOverride === false || commentsOverride === "false") {
return /* @__PURE__ */ jsx(Fragment, {});
}
const c = cfg;
return /* @__PURE__ */ jsx(
"div",
{
@ -32,7 +30,7 @@ var Comments_default = ((opts) => {
"data-input-position": opts.options.inputPosition ?? "bottom",
"data-light-theme": opts.options.lightTheme ?? "light",
"data-dark-theme": opts.options.darkTheme ?? "dark",
"data-theme-url": opts.options.themeUrl ?? `https://${c.baseUrl ?? "example.com"}/static/giscus`,
"data-theme-url": opts.options.themeUrl ?? `https://${cfg.baseUrl ?? "example.com"}/static/giscus`,
"data-lang": opts.options.lang ?? "en"
}
);

File diff suppressed because one or more lines are too long

8
dist/index.js vendored
View file

@ -12,12 +12,10 @@ function boolToStringBool(b) {
}
var Comments_default = ((opts) => {
const Comments = ({ displayClass, fileData, cfg }) => {
const fd = fileData;
const disableComment = typeof fd.frontmatter?.comments !== "undefined" && (!fd.frontmatter?.comments || fd.frontmatter?.comments === "false");
if (disableComment) {
const commentsOverride = fileData.frontmatter?.comments;
if (commentsOverride === false || commentsOverride === "false") {
return /* @__PURE__ */ jsx(Fragment, {});
}
const c = cfg;
return /* @__PURE__ */ jsx(
"div",
{
@ -32,7 +30,7 @@ var Comments_default = ((opts) => {
"data-input-position": opts.options.inputPosition ?? "bottom",
"data-light-theme": opts.options.lightTheme ?? "light",
"data-dark-theme": opts.options.darkTheme ?? "dark",
"data-theme-url": opts.options.themeUrl ?? `https://${c.baseUrl ?? "example.com"}/static/giscus`,
"data-theme-url": opts.options.themeUrl ?? `https://${cfg.baseUrl ?? "example.com"}/static/giscus`,
"data-lang": opts.options.lang ?? "en"
}
);

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

View file

@ -29,17 +29,11 @@ function boolToStringBool(b: boolean): string {
export default ((opts: CommentsOptions) => {
const Comments: QuartzComponent = ({ displayClass, fileData, cfg }: QuartzComponentProps) => {
// check if comments should be displayed according to frontmatter
const fd = fileData as Record<string, unknown>;
const disableComment: boolean =
typeof (fd.frontmatter as Record<string, unknown>)?.comments !== "undefined" &&
(!(fd.frontmatter as Record<string, unknown>)?.comments ||
(fd.frontmatter as Record<string, unknown>)?.comments === "false");
if (disableComment) {
const commentsOverride = fileData.frontmatter?.comments;
if (commentsOverride === false || commentsOverride === "false") {
return <></>;
}
const c = cfg as Record<string, unknown>;
return (
<div
class={classNames(displayClass, "giscus")}
@ -54,7 +48,7 @@ export default ((opts: CommentsOptions) => {
data-light-theme={opts.options.lightTheme ?? "light"}
data-dark-theme={opts.options.darkTheme ?? "dark"}
data-theme-url={
opts.options.themeUrl ?? `https://${(c.baseUrl as string) ?? "example.com"}/static/giscus`
opts.options.themeUrl ?? `https://${cfg.baseUrl ?? "example.com"}/static/giscus`
}
data-lang={opts.options.lang ?? "en"}
></div>

View file

@ -1,5 +1,60 @@
import { describe, it, expect } from "vitest";
import { Fragment } from "preact";
import Comments from "../src/components/Comments";
import type { QuartzComponent, QuartzComponentProps } from "@quartz-community/types";
type CommentsOptions = Parameters<typeof Comments>[0];
const baseOpts: CommentsOptions = {
provider: "giscus",
options: {
repo: "test/repo",
repoId: "test-id",
category: "Announcements",
categoryId: "test-cat-id",
},
};
function buildProps(
overrides: Partial<QuartzComponentProps> & {
frontmatter?: Record<string, unknown>;
cfg?: Record<string, unknown>;
} = {},
): QuartzComponentProps {
const { frontmatter, cfg, ...rest } = overrides;
return {
ctx: {},
externalResources: { css: [], js: [], additionalHead: [] },
fileData: { frontmatter: frontmatter ?? {} } as QuartzComponentProps["fileData"],
cfg: (cfg ?? {}) as QuartzComponentProps["cfg"],
children: [],
tree: null,
allFiles: [],
...rest,
};
}
function renderComments(
opts: CommentsOptions,
props: QuartzComponentProps,
): ReturnType<QuartzComponent> {
const Component = Comments(opts);
return Component(props);
}
function isFragmentOrEmpty(vnode: unknown): boolean {
if (vnode == null || vnode === false) return true;
if (typeof vnode !== "object") return false;
const v = vnode as { type?: unknown };
return v.type === Fragment;
}
function getDivProps(vnode: unknown): Record<string, unknown> | null {
if (typeof vnode !== "object" || vnode == null) return null;
const v = vnode as { type?: unknown; props?: Record<string, unknown> };
if (v.type !== "div") return null;
return v.props ?? null;
}
describe("Comments Plugin", () => {
it("should export Comments component", () => {
@ -7,16 +62,128 @@ describe("Comments Plugin", () => {
});
it("should create a component with options", () => {
const component = Comments({
provider: "giscus",
options: {
repo: "test/repo",
repoId: "test-id",
category: "Announcements",
categoryId: "test-cat-id",
},
});
const component = Comments(baseOpts);
expect(component).toBeDefined();
expect(typeof component).toBe("function");
});
});
describe("Comments: frontmatter.comments override", () => {
it("renders the giscus div when frontmatter.comments is undefined", () => {
const result = renderComments(baseOpts, buildProps({ frontmatter: {} }));
const props = getDivProps(result);
expect(props).not.toBeNull();
expect(props?.["data-repo"]).toBe("test/repo");
});
it("renders the giscus div when frontmatter.comments is true", () => {
const result = renderComments(baseOpts, buildProps({ frontmatter: { comments: true } }));
expect(getDivProps(result)).not.toBeNull();
});
it("renders the giscus div when frontmatter.comments is the string 'true'", () => {
const result = renderComments(baseOpts, buildProps({ frontmatter: { comments: "true" } }));
expect(getDivProps(result)).not.toBeNull();
});
it("suppresses comments when frontmatter.comments is false", () => {
const result = renderComments(baseOpts, buildProps({ frontmatter: { comments: false } }));
expect(isFragmentOrEmpty(result)).toBe(true);
expect(getDivProps(result)).toBeNull();
});
it("suppresses comments when frontmatter.comments is the string 'false'", () => {
const result = renderComments(baseOpts, buildProps({ frontmatter: { comments: "false" } }));
expect(isFragmentOrEmpty(result)).toBe(true);
expect(getDivProps(result)).toBeNull();
});
it("renders the giscus div when frontmatter.comments is the number 0", () => {
// 0 is falsy but not a documented disable value; it must not suppress comments.
const result = renderComments(baseOpts, buildProps({ frontmatter: { comments: 0 } }));
expect(getDivProps(result)).not.toBeNull();
});
it("renders the giscus div when frontmatter.comments is the empty string", () => {
// Empty string is falsy but not a documented disable value; it must not suppress comments.
const result = renderComments(baseOpts, buildProps({ frontmatter: { comments: "" } }));
expect(getDivProps(result)).not.toBeNull();
});
it("renders the giscus div when frontmatter.comments is null", () => {
// null is falsy but not a documented disable value; it must not suppress comments.
const result = renderComments(baseOpts, buildProps({ frontmatter: { comments: null } }));
expect(getDivProps(result)).not.toBeNull();
});
});
describe("Comments: data attribute wiring", () => {
it("propagates required giscus options", () => {
const result = renderComments(baseOpts, buildProps());
const props = getDivProps(result);
expect(props).not.toBeNull();
expect(props?.["data-repo"]).toBe("test/repo");
expect(props?.["data-repo-id"]).toBe("test-id");
expect(props?.["data-category"]).toBe("Announcements");
expect(props?.["data-category-id"]).toBe("test-cat-id");
});
it("applies default values when optional options are omitted", () => {
const result = renderComments(baseOpts, buildProps());
const props = getDivProps(result);
expect(props?.["data-mapping"]).toBe("url");
expect(props?.["data-strict"]).toBe("1");
expect(props?.["data-reactions-enabled"]).toBe("1");
expect(props?.["data-input-position"]).toBe("bottom");
expect(props?.["data-light-theme"]).toBe("light");
expect(props?.["data-dark-theme"]).toBe("dark");
expect(props?.["data-lang"]).toBe("en");
});
it("respects user-provided optional values", () => {
const opts: CommentsOptions = {
provider: "giscus",
options: {
...baseOpts.options,
mapping: "pathname",
strict: false,
reactionsEnabled: false,
inputPosition: "top",
lightTheme: "catppuccin_latte",
darkTheme: "catppuccin_mocha",
lang: "nl",
},
};
const result = renderComments(opts, buildProps());
const props = getDivProps(result);
expect(props?.["data-mapping"]).toBe("pathname");
expect(props?.["data-strict"]).toBe("0");
expect(props?.["data-reactions-enabled"]).toBe("0");
expect(props?.["data-input-position"]).toBe("top");
expect(props?.["data-light-theme"]).toBe("catppuccin_latte");
expect(props?.["data-dark-theme"]).toBe("catppuccin_mocha");
expect(props?.["data-lang"]).toBe("nl");
});
it("uses cfg.baseUrl to build the default themeUrl", () => {
const result = renderComments(baseOpts, buildProps({ cfg: { baseUrl: "example.test" } }));
const props = getDivProps(result);
expect(props?.["data-theme-url"]).toBe("https://example.test/static/giscus");
});
it("falls back when cfg.baseUrl is missing", () => {
const result = renderComments(baseOpts, buildProps({ cfg: {} }));
const props = getDivProps(result);
expect(props?.["data-theme-url"]).toBe("https://example.com/static/giscus");
});
it("honours an explicit themeUrl override", () => {
const opts: CommentsOptions = {
provider: "giscus",
options: { ...baseOpts.options, themeUrl: "https://cdn.test/theme" },
};
const result = renderComments(opts, buildProps({ cfg: { baseUrl: "example.test" } }));
const props = getDivProps(result);
expect(props?.["data-theme-url"]).toBe("https://cdn.test/theme");
});
});