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("| A | ")
+ })
+
+ 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("")
+ expect(html).not.toContain("")
+ expect(html).toContain("")
+ expect(html).toContain("| A | ")
+ })
+
+ 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("A | ")
+ expect(html).toContain("")
+ expect(html).toContain("C | ")
+ })
+})
+
+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("")
+ })
+})