WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Conversation

@naaa760
Copy link

@naaa760 naaa760 commented Nov 24, 2025

fix: #7254

Changes Overview

  • Fixed character count extension to trim from the end instead of the beginning when initial content exceeds the limit.

Implementation Approach

  • Updated the trimming logic to find the position where the limit is reached and delete from that position to the end, preserving the beginning text.

@changeset-bot
Copy link

changeset-bot bot commented Nov 24, 2025

⚠️ No Changeset found

Latest commit: 3e4feff

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@netlify
Copy link

netlify bot commented Nov 24, 2025

Deploy Preview for tiptap-embed ready!

Name Link
🔨 Latest commit 3e4feff
🔍 Latest deploy log https://app.netlify.com/projects/tiptap-embed/deploys/6923e73e6748210008f1b954
😎 Deploy Preview https://deploy-preview-7261--tiptap-embed.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@bdbch bdbch requested a review from Copilot December 3, 2025 19:31
Copilot finished reviewing on behalf of bdbch December 3, 2025 19:36
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes issue #7254 by changing the CharacterCount extension to trim text from the end instead of the beginning when initial content exceeds the character limit. Previously, the extension deleted characters from position 0 to over, removing the start of the content. Now it traverses the document to find where the limit is reached and deletes everything after that point.

Key changes:

  • Replaced simple range deletion (deleteRange(0, over)) with node traversal logic that finds the exact position where the character limit is reached
  • Added character-by-character counting within text nodes to handle the limit boundary accurately
  • Preserved content from the beginning of the document up to the limit
Comments suppressed due to low confidence (1)

packages/extensions/src/character-count/character-count.ts:163

  • This new trimming behavior when initial content exceeds the limit lacks test coverage. The repository has Cypress tests for other extensions (e.g., tests/cypress/integration/extensions/bold.spec.ts), and this critical behavior change should be tested to prevent regressions.

Consider adding a Cypress test that:

  1. Initializes an editor with content exceeding the character limit
  2. Verifies that content is trimmed from the end (not the beginning)
  3. Tests edge cases like multi-byte characters (emojis) and custom textCounter functions
  4. Ensures the resulting content is exactly at the limit
          if (initialContentSize > limit) {
            const doc = newState.doc
            let charCount = 0
            let limitPosition = doc.content.size

            doc.nodesBetween(0, doc.content.size, (node, pos) => {
              if (node.isText) {
                const nodeText = node.text || ''
                const nodeCharCount = this.options.textCounter(nodeText)

                if (charCount + nodeCharCount <= limit) {
                  charCount += nodeCharCount
                } else {
                  const remainingChars = limit - charCount
                  let charIndex = 0
                  let currentCharCount = 0

                  for (let i = 0; i < nodeText.length; i++) {
                    const substring = nodeText.slice(0, i + 1)
                    const substringCharCount = this.options.textCounter(substring)
                    
                    if (substringCharCount > remainingChars) {
                      break
                    }
                    
                    charIndex = i + 1
                    currentCharCount = substringCharCount
                  }

                  limitPosition = pos + 1 + charIndex
                  return false
                }
              }
            })

            console.warn(
              `[CharacterCount] Initial content exceeded limit of ${limit} characters. Content was automatically trimmed.`,
            )
            const tr = newState.tr.deleteRange(limitPosition, doc.content.size)

            initialEvaluationDone = true
            return tr
          }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

currentCharCount = substringCharCount
}

limitPosition = pos + 1 + charIndex
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The position calculation pos + 1 + charIndex is incorrect. In ProseMirror's nodesBetween, the pos parameter is the position before the node. For text nodes, you need to account for the opening token of the parent node.

The correct calculation should be pos + charIndex (without the + 1), because:

  • pos is the position before the text node content starts
  • charIndex is the offset within the text node where the limit is reached

The current implementation will delete one character less than intended, leaving content that exceeds the limit.

Suggested change
limitPosition = pos + 1 + charIndex
limitPosition = pos + charIndex

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +147
let currentCharCount = 0

for (let i = 0; i < nodeText.length; i++) {
const substring = nodeText.slice(0, i + 1)
const substringCharCount = this.options.textCounter(substring)

if (substringCharCount > remainingChars) {
break
}

charIndex = i + 1
currentCharCount = substringCharCount
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable currentCharCount is assigned but never used. Consider removing it to reduce confusion.

Copilot uses AI. Check for mistakes.
Comment on lines +126 to +154
doc.nodesBetween(0, doc.content.size, (node, pos) => {
if (node.isText) {
const nodeText = node.text || ''
const nodeCharCount = this.options.textCounter(nodeText)

if (charCount + nodeCharCount <= limit) {
charCount += nodeCharCount
} else {
const remainingChars = limit - charCount
let charIndex = 0
let currentCharCount = 0

for (let i = 0; i < nodeText.length; i++) {
const substring = nodeText.slice(0, i + 1)
const substringCharCount = this.options.textCounter(substring)

if (substringCharCount > remainingChars) {
break
}

charIndex = i + 1
currentCharCount = substringCharCount
}

limitPosition = pos + 1 + charIndex
return false
}
}
})
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback in nodesBetween doesn't handle non-text nodes, which means they are skipped in the traversal. If the document contains structural nodes (paragraphs, headings, etc.) between text nodes, the position calculation will be incorrect because those nodes occupy positions in the document.

Additionally, if all text nodes fit within the limit but their total still exceeds it (due to structural node sizes in nodeSize mode, though this code appears to use textSize), limitPosition will remain at doc.content.size and nothing will be deleted, despite the content exceeding the limit.

Consider either:

  1. Explicitly handling all node types in the traversal, or
  2. Using textBetween to get the text and calculate positions more reliably, or
  3. Adding a fallback check after the loop to verify the limit is actually being enforced

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +147
const substring = nodeText.slice(0, i + 1)
const substringCharCount = this.options.textCounter(substring)

if (substringCharCount > remainingChars) {
break
}

charIndex = i + 1
currentCharCount = substringCharCount
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested loop with repeated textCounter calls has O(n²) time complexity. For each character position i, you're calling textCounter(nodeText.slice(0, i + 1)), which may iterate through all characters up to i again (especially for complex counters like Intl.Segmenter).

Consider optimizing by incrementally counting characters instead of recalculating from scratch each time:

let charIndex = 0
for (let i = 0; i < nodeText.length; i++) {
  const char = nodeText[i]
  const charLength = this.options.textCounter(char)
  
  if (charCount + charLength > limit) {
    break
  }
  
  charCount += charLength
  charIndex = i + 1
}

Note: This assumes the textCounter is additive. For complex counters (like grapheme clusters), you may need a different approach, but the current implementation is still problematic for long text.

Suggested change
const substring = nodeText.slice(0, i + 1)
const substringCharCount = this.options.textCounter(substring)
if (substringCharCount > remainingChars) {
break
}
charIndex = i + 1
currentCharCount = substringCharCount
const char = nodeText[i]
const charLength = this.options.textCounter(char)
if (currentCharCount + charLength > remainingChars) {
break
}
currentCharCount += charLength
charIndex = i + 1

Copilot uses AI. Check for mistakes.
}

charIndex = i + 1
currentCharCount = substringCharCount
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value assigned to currentCharCount here is unused.

Copilot uses AI. Check for mistakes.
Copy link
Member

@bdbch bdbch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the copilot comments. There are a few functional and performance related problems with the PR (bad-scaling loops, positions being calculated off, etc.)

I would also like to have a test for this.

Feel free to add a vitest unit test in packages/extensions/src/character-count/__test__ that tests those cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Character count extension trims text from the beginning instead of the end when content exceeds limit

2 participants