From d4d213b2a8ab9a7930595346ca017d423a7531fc Mon Sep 17 00:00:00 2001 From: Yuzhong Zhang Date: Tue, 1 Sep 2026 12:27:47 +0000 Subject: [PATCH] Preserve numeric 0 when converting YAML directive options parseDirectiveOptions stringified option values with `|| ""`, so YAML numeric 0 became an empty string and nonnegative_int threw "Value is not set". Use nullish coalescing so 0 is kept; null/false still map to empty string first. --- src/directives/main.ts | 2 +- tests/listTableHeaderRows.spec.ts | 92 +++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 tests/listTableHeaderRows.spec.ts diff --git a/src/directives/main.ts b/src/directives/main.ts index b8665a8..bc8d09d 100644 --- a/src/directives/main.ts +++ b/src/directives/main.ts @@ -294,7 +294,7 @@ export function parseDirectiveOptions( // In docutils all values are simply read as strings, // but loading with YAML these can be converted to other types, so we convert them back first // TODO check that it is sufficient to simply do this conversion, or if there is a better way - converted_value = convertor(`${converted_value || ""}`) + converted_value = convertor(`${converted_value ?? ""}`) } catch (error) { throw new DirectiveParsingError( `Invalid option value: (option: '${name}'; value: ${value})\n${error}` diff --git a/tests/listTableHeaderRows.spec.ts b/tests/listTableHeaderRows.spec.ts new file mode 100644 index 0000000..7fef261 --- /dev/null +++ b/tests/listTableHeaderRows.spec.ts @@ -0,0 +1,92 @@ +import MarkdownIt from "markdown-it" +import docutils_plugin from "../src" +import { parseDirectiveOptions } from "../src/directives/main" +import { nonnegative_int, unchanged } from "../src/directives/options" + +function render(src: string): string { + return MarkdownIt().use(docutils_plugin).render(src) +} + +const listTableBody = `* - A + - B +* - C + - D` + +describe("list-table header-rows: 0 is a valid value", () => { + it("YAML option block header-rows: 0 must not throw OptionSpecError / Value is not set", () => { + const html = render(`\`\`\`{list-table} +--- +header-rows: 0 +--- +${listTableBody} +\`\`\``) + expect(html).not.toMatch(/OptionSpecError/) + expect(html).not.toMatch(/Value is not set/) + expect(html).not.toMatch(/directive-error/) + expect(html).toContain("") + expect(html).not.toContain("") + expect(html).toContain("") + expect(html).toContain("") + }) + + it("colon form :header-rows: 0 must also accept numeric 0", () => { + const html = render(`\`\`\`{list-table} +:header-rows: 0 + +${listTableBody} +\`\`\``) + expect(html).not.toMatch(/OptionSpecError/) + expect(html).not.toMatch(/Value is not set/) + expect(html).not.toMatch(/directive-error/) + expect(html).toContain("
A
") + expect(html).not.toContain("") + expect(html).toContain("") + expect(html).toContain("") + }) + + it("header-rows: 1 must still work (thead present)", () => { + const html = render(`\`\`\`{list-table} +:header-rows: 1 + +${listTableBody} +\`\`\``) + expect(html).not.toMatch(/directive-error/) + expect(html).toContain("") + expect(html).toContain("") + expect(html).toContain("") + expect(html).toContain("") + }) +}) + +describe("parseDirectiveOptions preserves numeric 0", () => { + const spec = { option_spec: { "header-rows": nonnegative_int } } + + it("YAML header-rows: 0 converts to number 0, not empty string", () => { + const [, options] = parseDirectiveOptions(["---", "header-rows: 0", "---"], spec) + expect(options["header-rows"]).toBe(0) + }) + + it("colon :header-rows: 0 converts to number 0 (yaml.load yields 0)", () => { + const [, options] = parseDirectiveOptions([":header-rows: 0"], spec) + expect(options["header-rows"]).toBe(0) + }) + + it("header-rows: 1 still converts to number 1", () => { + const [, options] = parseDirectiveOptions([":header-rows: 1"], spec) + expect(options["header-rows"]).toBe(1) + }) + + it("null option values still become empty string before convertor", () => { + const [, options] = parseDirectiveOptions(["---", "a:", "---"], { + option_spec: { a: unchanged } + }) + expect(options.a).toBe("") + }) + + it("false option values still become empty string before convertor", () => { + const [, options] = parseDirectiveOptions(["---", "a: false", "---"], { + option_spec: { a: unchanged } + }) + expect(options.a).toBe("") + }) +})
A
A
C