diff --git a/javascript/packages/formatter/src/format-helpers.ts b/javascript/packages/formatter/src/format-helpers.ts index 1c592b4b0..56abd291e 100644 --- a/javascript/packages/formatter/src/format-helpers.ts +++ b/javascript/packages/formatter/src/format-helpers.ts @@ -194,7 +194,9 @@ export function isERBTag(text: string): boolean { * Check if a string ends with an ERB tag */ export function endsWithERBTag(text: string): boolean { - return /%>$/.test(text.trim()) + const trimmed = text.trim() + + return /%>$/.test(trimmed) || /%>\S+$/.test(trimmed) } /** diff --git a/javascript/packages/formatter/src/format-printer.ts b/javascript/packages/formatter/src/format-printer.ts index f19f9fe30..6dd97ec57 100644 --- a/javascript/packages/formatter/src/format-printer.ts +++ b/javascript/packages/formatter/src/format-printer.ts @@ -1862,7 +1862,7 @@ export class FormatPrinter extends Printer { const firstWord = words[0] const firstChar = firstWord[0] - if (!/[a-zA-Z0-9.!?:;]/.test(firstChar)) { + if (/\s/.test(firstChar)) { return false } @@ -1879,6 +1879,11 @@ export class FormatPrinter extends Printer { unit: { content: remainingText, type: 'text', isAtomic: false, breaksFlow: false }, node: textNode }) + } else if (endsWithWhitespace(textNode.content)) { + result.push({ + unit: { content: ' ', type: 'text', isAtomic: false, breaksFlow: false }, + node: textNode + }) } return true @@ -1955,10 +1960,8 @@ export class FormatPrinter extends Printer { const hasWhitespace = this.hasWhitespaceBeforeNode(children, lastProcessedIndex, index, child) const lastUnit = result[result.length - 1] const lastIsAtomic = lastUnit.unit.isAtomic && (lastUnit.unit.type === 'erb' || lastUnit.unit.type === 'inline') - const trimmed = child.content.trim() - const startsWithClosingPunct = trimmed.length > 0 && /^[.!?:;]/.test(trimmed) - if (lastIsAtomic && (!hasWhitespace || startsWithClosingPunct) && this.tryMergeTextAfterAtomic(result, child)) { + if (lastIsAtomic && !hasWhitespace && this.tryMergeTextAfterAtomic(result, child)) { return } } diff --git a/javascript/packages/formatter/test/erb/erb.test.ts b/javascript/packages/formatter/test/erb/erb.test.ts index a2f505e7f..c44301545 100644 --- a/javascript/packages/formatter/test/erb/erb.test.ts +++ b/javascript/packages/formatter/test/erb/erb.test.ts @@ -165,7 +165,7 @@ describe("@herb-tools/formatter", () => { expect(result).toBe(dedent`
+ Lorem <%= letter.patient.first_name.titlecase %>'s ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +
+ ` + const result = formatter.format(source) + + expect(result).toEqual(dedent` ++ Lorem <%= letter.patient.first_name.titlecase %>'s ipsum dolor sit amet, + consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. +
+ `) + }) + + test("preserves dollar sign before ERB tag without adding space", () => { + const source = `Lorem $<%= value %> ipsum dolor sit amet.
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves euro symbol after ERB tag without adding space", () => { + const source = `Lorem <%= value %>€ ipsum dolor sit amet.
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves hash symbol before ERB tag without adding space", () => { + const source = `Lorem #<%= value %> ipsum dolor sit amet.
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("keeps hyphen attached between adjacent ERB tags", () => { + const source = `Lorem <%= value %>-<%= value %> ipsum dolor sit amet.
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("keeps period attached between adjacent ERB tags", () => { + const source = `Lorem <%= value %>.<%= value %> ipsum dolor sit amet.
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves punctuation sequence around ERB tags without spaces", () => { + const source = `Lorem .<%= value %>.<%= value %>. ipsum dolor sit amet.
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves punctuation sequence around ERB tags with spaces", () => { + const source = `Lorem . <%= value %> . <%= value %> . ipsum dolor sit amet.
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("keeps adjacent ERB tags together without adding space", () => { + const source = `Lorem <%= one %><%= two %> ipsum dolor sit amet.
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("formats standalone period after block element on new line", () => { + const source = `hello
.` + const result = formatter.format(source) + expect(result).toEqual(dedent` +hello
+ + . + `) + }) + + test("formats period after closing tag within parent block", () => { + const source = dedent` +hello
. +hello
+ . +Hello World. Hello
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves period spacing after inline element", () => { + const source = `Hello World . Hello
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + }) + + describe("edge cases and special characters", () => { + test("preserves exclamation mark after ERB tag", () => { + const source = `Hello <%= name %>!
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves question mark after ERB tag", () => { + const source = `Are you <%= adjective %>?
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves colon after ERB tag", () => { + const source = `Result: <%= value %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves semicolon after ERB tag", () => { + const source = `First <%= value %>; then <%= value2 %>.
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves multiple punctuation marks (ellipsis)", () => { + const source = `Loading<%= dots %>...
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves multiple exclamation marks", () => { + const source = `Alert<%= message %>!!!
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves quotes around ERB tag", () => { + const source = `He said "<%= quote %>".
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves single quotes around ERB tag", () => { + const source = `Word: '<%= word %>'
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves parentheses around ERB tag", () => { + const source = `Details (<%= info %>)
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves brackets around ERB tag", () => { + const source = `Index [<%= index %>]
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves slash between ERB tags (fraction)", () => { + const source = `<%= numerator %>/<%= denominator %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves slash after ERB tag (file path)", () => { + const source = `<%= dir %>/<%= file %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves backslash after ERB tag", () => { + const source = `<%= path %>\\<%= file %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves at-sign before ERB tag (mention)", () => { + const source = `@<%= username %> said hello
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves hashtag with ERB tag", () => { + const source = `#<%= tag %> is trending
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves percent sign after ERB tag", () => { + const source = `<%= value %>%
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves ampersand between ERB tags", () => { + const source = `<%= first %>&<%= second %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves plus sign between ERB tags (concatenation)", () => { + const source = `<%= a %>+<%= b %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves asterisk between ERB tags (multiplication)", () => { + const source = `<%= width %>*<%= height %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves equals sign between ERB tags", () => { + const source = `<%= key %>=<%= value %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves caret after ERB tag", () => { + const source = `<%= base %>^<%= exponent %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves tilde before ERB tag", () => { + const source = `~<%= approximate %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves pipe between ERB tags", () => { + const source = `<%= option1 %>|<%= option2 %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves underscore between ERB tags", () => { + const source = `<%= first %>_<%= last %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves numbers after ERB tag", () => { + const source = `<%= value %>123
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves numbers before ERB tag", () => { + const source = `Version 123<%= suffix %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves decimal point in price format", () => { + const source = `$<%= dollars %>.<%= cents %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves colon in time format", () => { + const source = `<%= hours %>:<%= minutes %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves x in dimensions format", () => { + const source = `<%= width %>x<%= height %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves file extension with ERB tag", () => { + const source = `<%= filename %>.html
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves multiple file extensions", () => { + const source = `<%= filename %>.html.erb
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves em dash between ERB tags", () => { + const source = `<%= start %>—<%= end %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves en dash between ERB tags", () => { + const source = `<%= start %>–<%= end %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("handles ERB comment with apostrophe", () => { + const source = `Hello <%# user's name %><%= name %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves possessive with multiple ERB tags", () => { + const source = `<%= first_name %> <%= last_name %>'s profile
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves multiple apostrophes in sequence", () => { + const source = `<%= name %>'s friend's house
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves abbreviation with ERB tag", () => { + const source = `Dr.<%= name %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves trailing abbreviation", () => { + const source = `<%= name %> Ph.D.
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves complex punctuation sequence", () => { + const source = `"<%= title %>"—<%= author %>'s masterpiece!
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves comma and space between ERB tags", () => { + const source = `<%= city %>, <%= state %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("handles mixed punctuation and text", () => { + const source = `<%= value %>: <%= description %> (<%= note %>).
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves angle brackets (comparison operators)", () => { + const source = `<%= a %><<%= b %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves greater than with ERB tags", () => { + const source = `<%= a %>><%= b %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("handles nested quotes and apostrophes", () => { + const source = `"<%= name %>'s quote"
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves contractions before ERB tag", () => { + const source = `can't <%= verb %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves contractions after ERB tag", () => { + const source = `<%= subject %> can't <%= verb %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("handles backticks around ERB tag (code)", () => { + const source = `Use \`<%= code %>\` here
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves currency symbol before decimal ERB tag", () => { + const source = `$<%= price %>.99
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves comma in large numbers", () => { + const source = `<%= thousands %>,<%= hundreds %>
` + const result = formatter.format(source) + expect(result).toEqual(source) + }) }) describe("shared utility validation", () => {