Skip to content
Merged
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
11 changes: 8 additions & 3 deletions javascript/packages/formatter/src/format-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,19 +398,24 @@ export function isContentPreserving(element: HTMLElementNode | HTMLOpenTagNode |
}

/**
* Count consecutive inline elements/ERB at the start of children (with no whitespace between)
* Count consecutive inline elements/ERB with no whitespace between them.
* Starts from startIndex and skips indices in processedIndices.
*/
export function countAdjacentInlineElements(children: Node[]): number {
export function countAdjacentInlineElements(children: Node[], startIndex = 0, processedIndices?: Set<number>): number {
let count = 0
let lastSignificantIndex = -1

for (let i = 0; i < children.length; i++) {
for (let i = startIndex; i < children.length; i++) {
const child = children[i]

if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
continue
}

if (processedIndices?.has(i)) {
break
}

const isInlineOrERB = (isNode(child, HTMLElementNode) && isInlineElement(getTagName(child))) || isNode(child, ERBContentNode)

if (!isInlineOrERB) {
Expand Down
47 changes: 38 additions & 9 deletions javascript/packages/formatter/src/format-printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1792,7 +1792,7 @@ export class FormatPrinter extends Printer {
}
}

const match = trimmedText.match(/^[.!?:;]+/)
const match = trimmedText.match(/^[.!?:;%]+/)

if (!match) {
return {
Expand Down Expand Up @@ -1851,19 +1851,23 @@ export class FormatPrinter extends Printer {
/**
* Render adjacent inline elements together on one line
*/
private renderAdjacentInlineElements(children: Node[], count: number): { processedIndices: Set<number> } {
private renderAdjacentInlineElements(children: Node[], count: number, startIndex = 0, alreadyProcessed?: Set<number>): { processedIndices: Set<number>; lastIndex: number } {
let inlineContent = ""
let processedCount = 0
let lastProcessedIndex = -1
const processedIndices = new Set<number>()

for (let index = 0; index < children.length && processedCount < count; index++) {
for (let index = startIndex; index < children.length && processedCount < count; index++) {
const child = children[index]

if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
continue
}

if (alreadyProcessed?.has(index)) {
continue
}

if (isNode(child, HTMLElementNode) && isInlineElement(getTagName(child))) {
inlineContent += this.renderInlineElementAsString(child)
processedCount++
Expand All @@ -1890,21 +1894,27 @@ export class FormatPrinter extends Printer {
continue
}

if (alreadyProcessed?.has(index)) {
break
}

if (isNode(child, ERBContentNode)) {
inlineContent += this.renderERBAsString(child)
processedIndices.add(index)
lastProcessedIndex = index
continue
}

if (isNode(child, HTMLTextNode)) {
const trimmed = child.content.trim()

if (trimmed && /^[.!?:;]/.test(trimmed)) {
if (trimmed && /^[.!?:;%]/.test(trimmed)) {
const wrapWidth = this.maxLineLength - this.indent.length
const result = this.tryMergePunctuationText(inlineContent, trimmed, wrapWidth)

inlineContent = result.mergedContent
processedIndices.add(index)
lastProcessedIndex = index

if (result.shouldStop) {
if (inlineContent) {
Expand All @@ -1913,7 +1923,7 @@ export class FormatPrinter extends Printer {

result.wrappedLines.forEach(line => this.push(line))

return { processedIndices }
return { processedIndices, lastIndex: lastProcessedIndex }
}
}
}
Expand All @@ -1926,7 +1936,10 @@ export class FormatPrinter extends Printer {
this.pushWithIndent(inlineContent)
}

return { processedIndices }
return {
processedIndices,
lastIndex: lastProcessedIndex >= 0 ? lastProcessedIndex : startIndex + count - 1
}
}

/**
Expand Down Expand Up @@ -1964,21 +1977,37 @@ export class FormatPrinter extends Printer {
}

/**
* Visit remaining children after processing adjacent inline elements
* Visit remaining children after processing adjacent inline elements.
* Detects and renders subsequent groups of adjacent inline elements.
*/
private visitRemainingChildren(children: Node[], processedIndices: Set<number>): void {
for (let index = 0; index < children.length; index++) {
let index = 0

while (index < children.length) {
const child = children[index]

if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
index++
continue
}

if (processedIndices.has(index)) {
index++
continue
}

this.visit(child)
const adjacentCount = countAdjacentInlineElements(children, index, processedIndices)

if (adjacentCount >= 2) {
const { processedIndices: newProcessedIndices, lastIndex } =
this.renderAdjacentInlineElements(children, adjacentCount, index, processedIndices)

newProcessedIndices.forEach(i => processedIndices.add(i))
index = lastIndex + 1
} else {
this.visit(child)
index++
}
}
}

Expand Down
24 changes: 24 additions & 0 deletions javascript/packages/formatter/test/erb/erb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,30 @@ describe("@herb-tools/formatter", () => {
expect(secondFormat).toBe(result)
})

test("issue 978: does not duplicate content with multiple adjacent inline/ERB groups separated by br", () => {
const input = dedent`
<p class="text-reversed">
<strong><%= t("admin.tickets.form.savings") %></strong><%= ticket.ticketable.savings_percentage %>%
<br>
<strong><%= t("admin.tickets.form.price_per_ticket") %></strong><%= format_price(ticket.ticketable.price_per_ticket) %>
</p>
`

const result = formatter.format(input)

expect(result).toBe(dedent`
<p class="text-reversed">
<strong><%= t("admin.tickets.form.savings") %></strong><%= ticket.ticketable.savings_percentage %>%
<br>
<strong><%= t("admin.tickets.form.price_per_ticket") %></strong><%= format_price(ticket.ticketable.price_per_ticket) %>
</p>
`)

// Test idempotency
const secondFormat = formatter.format(result)
expect(secondFormat).toBe(result)
})

test("https://github.com/hanakai-rb/site/blob/8adc128d9d464f3e37615be2aa29d57979904533/app/templates/pages/home.html.erb", () => {
const input = dedent`
<h1>Hanakai</h1>
Expand Down
Loading