diff --git a/packages/comments/README.md b/packages/comments/README.md index ed54101c..829aa1f2 100644 --- a/packages/comments/README.md +++ b/packages/comments/README.md @@ -116,6 +116,7 @@ interface FileResult { ## Heuristics Overview - Adds a terminal period to sentences lacking final punctuation (unless ending with a colon, tag line, list marker, or detected continuation). +- Treats `@tag` lines as JSDoc tags, except scoped package names (`@auth0/auth0-react`) and CSS at-rules (`@keyframes`, `@media`, `@layer`, ...) that open a wrapped prose line; those stay part of the sentence. - Normalizes `NOTE:` capitalization and splits multiple NOTE sentences safely. - Skips wrapping/merging for linter and tool directive comments, including: - ESLint (`eslint-disable`, `eslint-enable`, `eslint-disable-next-line`, etc.) diff --git a/packages/comments/src/__tests__/lib.test.ts b/packages/comments/src/__tests__/lib.test.ts index 1932e5b8..cb390963 100644 --- a/packages/comments/src/__tests__/lib.test.ts +++ b/packages/comments/src/__tests__/lib.test.ts @@ -114,6 +114,77 @@ describe("parseAndTransformComments", () => { expect(transformed).toMatch(/future @auth0\/auth0-react/); }); + it("does not treat a CSS at-rule as a JSDoc tag", () => { + const cases: [string, RegExp, RegExp][] = [ + [ + [ + "/**", + " * Global styles for the UI. Animations are registered through the", + " * @keyframes rule and referenced by name in each component.", + " */", + ].join("\n") + "\n", + /through the\./, + /through the @keyframes/, + ], + [ + [ + "/**", + " * Fade the panel in. Uses", + " * @media (prefers-reduced-motion) to disable the transition entirely.", + " */", + ].join("\n") + "\n", + /Uses\./, + /Uses @media \(prefers-reduced-motion\)/, + ], + [ + [ + "/**", + " * Layout tokens live under the", + " * @layer base so they can be overridden by utilities.", + " */", + ].join("\n") + "\n", + /under the\./, + /under the @layer/, + ], + ]; + for (const [input, spuriousPeriod, joined] of cases) { + const { transformed } = parseAndTransformComments(input, { + width: 80, + wrapLineComments: true, + mergeLineComments: true, + }); + // No period injected mid-sentence. + expect(transformed).not.toMatch(spuriousPeriod); + // Sentence joined, at-rule kept inline as prose. + expect(transformed).toMatch(joined); + } + }); + + it("still treats JSDoc tags that share a name with a CSS at-rule as tags", () => { + const input = + [ + "/**", + " * Describes a thing", + " * @property {string} name The display name", + " * @function", + " * @namespace Foo", + " * @return {number} the result", + " */", + ].join("\n") + "\n"; + const { transformed } = parseAndTransformComments(input, { + width: 80, + wrapLineComments: true, + mergeLineComments: true, + }); + expect(transformed).toMatch(/^ \* Describes a thing\.$/m); + expect(transformed).toMatch( + /^ \* @property \{string\} name The display name$/m, + ); + expect(transformed).toMatch(/^ \* @function$/m); + expect(transformed).toMatch(/^ \* @namespace Foo$/m); + expect(transformed).toMatch(/^ \* @return \{number\} the result$/m); + }); + it("still treats real JSDoc tags as tags after the scoped-package fix", () => { const input = "/**\n * Does a thing.\n * @param x in\n * @returns out\n */\n"; diff --git a/packages/comments/src/lib.ts b/packages/comments/src/lib.ts index c245b480..a93c31db 100644 --- a/packages/comments/src/lib.ts +++ b/packages/comments/src/lib.ts @@ -164,15 +164,51 @@ function isListLike(line: string): boolean { return /^(?:[-*+] |\d+\. )/.test(line.trim()); } +/** + * CSS at-rules that can legitimately open a wrapped prose line in a comment + * describing styles (e.g. "... are registered through the" followed by + * "@keyframes rule and referenced by name"). None of these names doubles as a + * JSDoc/TSDoc tag, so a line starting with one of them is sentence + * continuation, not a tag. At-rules whose name IS also a JSDoc tag (@function, + * @import, @mixin, @namespace, @property, @return) are deliberately absent so + * real tags keep working. + */ +const CSS_AT_RULES = new Set([ + "apply", + "charset", + "color-profile", + "container", + "counter-style", + "font-face", + "font-feature-values", + "font-palette-values", + "keyframes", + "layer", + "media", + "page", + "position-try", + "scope", + "screen", + "starting-style", + "supports", + "tailwind", + "view-transition", +]); + function isTagLine(line: string): boolean { /** * A JSDoc/TSDoc tag is `@name` (optionally hyphenated) and is never * immediately followed by `/`. The negative lookahead excludes scoped npm * package specifiers like `@auth0/auth0-react` or `@versini/ui-main` that can * legitimately open a wrapped prose line; treating those as tags severs the - * sentence and injects a spurious period. + * sentence and injects a spurious period. CSS at-rules (see CSS_AT_RULES) are + * excluded for the same reason. */ - return /^@[A-Za-z][\w-]*(?![\w/-])/.test(line.trim()); + const m = /^@([A-Za-z][\w-]*)(?![\w/-])/.exec(line.trim()); + if (!m) { + return false; + } + return !CSS_AT_RULES.has(m[1].toLowerCase()); } function isHeadingLike(line: string): boolean {