Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ title: Changelog
### Bug Fixes

- Anchor links in the mobile hamburger menu now correctly scroll to their target, #3049.
- Member names containing non-ASCII characters are no longer rendered as quoted strings in type signatures if they are valid identifiers.

## v0.28.20 (2026-07-05)

Expand Down
25 changes: 24 additions & 1 deletion src/lib/output/formatter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,35 @@ import {
} from "#models";
import { aggregate, assertNever, JSX } from "#utils";
import { ok } from "assert";
import ts from "typescript";
import type { Router } from "./index.js";
import { getKindClass, getUniquePath, stringify } from "./themes/lib.js";

// Non breaking space
const INDENT = "\u00A0\u00A0\u00A0\u00A0";

function isIdentifierText(name: string): boolean {
let codePoint = name.codePointAt(0);
if (
codePoint === undefined ||
!ts.isIdentifierStart(codePoint, ts.ScriptTarget.Latest)
) {
return false;
}

for (
let offset = codePoint > 0xffff ? 2 : 1;
offset < name.length;
offset += codePoint > 0xffff ? 2 : 1
) {
codePoint = name.codePointAt(offset)!;
if (!ts.isIdentifierPart(codePoint, ts.ScriptTarget.Latest)) {
return false;
}
}
return true;
}

export type FormatterNode =
| { type: "text"; content: string }
| { type: "element"; content: JSX.Element; length: number }
Expand Down Expand Up @@ -1143,7 +1166,7 @@ export class FormattedCodeBuilder {
reflection: Reflection,
options: { topLevelLinks?: boolean },
): FormatterNode {
const entityName = /^[A-Z_$][\w$]*$/i.test(reflection.name)
const entityName = isIdentifierText(reflection.name)
? reflection.name
: JSON.stringify(reflection.name);

Expand Down
77 changes: 77 additions & 0 deletions src/test/output/formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,83 @@ describe("Formatter", () => {
);
});

it("Does not quote non-ASCII member names which are valid identifiers", () => {
const refl = new DeclarationReflection(
"__type",
ReflectionKind.TypeLiteral,
);
for (const name of ["café", "Größe", "日本語", "имя", "zw\u200Cnj"]) {
const child = new DeclarationReflection(
name,
ReflectionKind.Property,
refl,
);
child.type = new IntrinsicType("string");
refl.addChild(child);
}

const type = new ReflectionType(refl);
const text = renderElementToText(renderType(type));
equal(
text,
"{ café: string; Größe: string; 日本語: string; имя: string; zw\u200Cnj: string }",
);

const textWrap = renderElementToText(renderType(type, 0));
equal(
textWrap,
dedent(`
{
café: string;
Größe: string;
日本語: string;
имя: string;
zw\u200Cnj: string;
}
`),
);
});

it("Quotes member names which are not valid identifiers", () => {
const refl = new DeclarationReflection(
"__type",
ReflectionKind.TypeLiteral,
);
for (const name of ["[iterator]", "0", "a-b", "with space"]) {
const child = new DeclarationReflection(
name,
ReflectionKind.Property,
refl,
);
child.type = new IntrinsicType("string");
refl.addChild(child);
}

const type = new ReflectionType(refl);
const text = renderElementToText(renderType(type));
equal(
text,
`{ "[iterator]": string; "0": string; "a-b": string; "with space": string }`,
);
});

it("Uses TypeScript's identifier table", () => {
const refl = new DeclarationReflection(
"__type",
ReflectionKind.TypeLiteral,
);
const child = new DeclarationReflection(
"\u088F",
ReflectionKind.Property,
refl,
);
child.type = new IntrinsicType("string");
refl.addChild(child);

const type = new ReflectionType(refl);
equal(renderElementToText(renderType(type)), `{ "\u088F": string }`);
});

it("Handles rest types", () => {
const type = new RestType(new LiteralType("x"));
const text = renderElementToText(renderType(type));
Expand Down
Loading