Skip to content

fix(tiptap-editor): stop inserting unrenderable content as literal markup - #1657

Merged
feruzm merged 4 commits into
developfrom
fix/paste-renders-as-nothing
Aug 24, 2026
Merged

fix(tiptap-editor): stop inserting unrenderable content as literal markup#1657
feruzm merged 4 commits into
developfrom
fix/paste-renders-as-nothing

Conversation

@feruzm

@feruzm feruzm commented Aug 24, 2026

Copy link
Copy Markdown
Member

Fixes #1656, and picks up the three smaller items from the #1655 review that were left out of it.

The leak (#1656)

When a paste parses to an empty slice, insertContentAt never falsifies its isOnlyTextContent flag (the forEach over the nodes runs zero times) and falls through to tr.insertText with the HTML string, so an iframe embed lands in the document as visible <iframe src=...>. The same branch double-escapes text-only content: A & B arrives as those literal characters. Verified identical in jsdom and real Chromium via Playwright, so it is tiptap behaviour, not a DOM quirk.

resolveInsertContent decides what to hand insertContent:

parse result inserted
empty the fallback text, or nothing when there is none
text alone the decoded text, not the HTML that encoded it
anything else the HTML, exactly as before

All three insertContent callers use it: the clipboard text strategy, the translate dialog, and the toolbar fragments. The two setContent callers are unaffected, since setContent goes through createDocument and never reaches this branch.

Why the fallback is the user's own clipboard text. Both before and after, the result is literal text, so "is it escaped" cannot distinguish them. What differs is whose text it is: ours is sanitised and attribute-normalised, and a pasted <video src=x controls> reached the document as <video controls="">, having quietly dropped the URL. The spec asserts that, rather than asserting on escaping.

The three smaller items

  • Zero-width characters count as invisible. trim() drops U+00A0 but not U+200B, U+200D, U+2060 or U+FEFF, so an item holding only one of those counted as content and rendered as a blank-looking bullet. One hasVisibleText helper now backs every emptiness check in the file.
  • The jsdom limits are written down in the spec helper: no innerText, and jsdom and Chromium genuinely disagree on foster parenting of loose text inside a table, so document order for that shape must be checked in a browser rather than asserted in a spec.
  • The empty <tr> was a false alarm and is not fixed here. It was reported as the same schema class, but tableRow is (tableCell | tableHeader)*, not +, so an empty row is valid and nothing throws. Measured before writing any code.

Verification

New spec fails 4 of 10 cases against the old insert path; the zero-width cases fail on develop. Full web suite 3543 passing, tsc clean, lint clean.

…rkup

When a paste parses to an empty slice, tiptap's insertContentAt never falsifies
its isOnlyTextContent flag (the forEach over the nodes runs zero times) and
falls through to tr.insertText with the HTML STRING, so pasting an iframe embed
put `&lt;iframe src=...&gt;` in the document as visible text. The same branch
double-escapes text-only content: `A &amp; B` arrived as those characters.
Confirmed identical in jsdom and real Chromium, so this is tiptap behaviour
rather than a DOM quirk.

resolveInsertContent decides what to hand insertContent: the fallback text when
nothing renders, the decoded text when the parse is text alone, the HTML
otherwise. All three insertContent callers use it.

The fallback is the user's own clipboard text rather than our normalised HTML.
Both end up as literal text, so escaping cannot tell them apart, but ours is
sanitised and attribute-normalised: a pasted `<video src=x controls>` reached
the document as `<video controls="">`, having quietly lost the URL. The spec
asserts whose text it is, not whether it is escaped.

Also treats zero-width characters as invisible. trim() drops U+00A0 but not
U+200B, U+200D, U+2060 or U+FEFF, so an item holding only one of those counted
as content and rendered as a blank-looking bullet.

And records in the spec helper the two things these specs cannot settle:
jsdom has no innerText, and jsdom and Chromium disagree on foster parenting of
loose text inside a table.

Closes #1656
@qodo-code-review

Copy link
Copy Markdown

ⓘ Your Qodo trial ends soon. Ask your workspace admin to set up billing to keep reviews running after the trial. Manage billing

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 24, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. hasVisibleText missing return type ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new helper hasVisibleText has no explicit return type annotation, relying on inference. This
violates the rule prohibiting implicit types in newly added/modified TypeScript code and can allow
type drift if the implementation changes.
Code

apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[R99-102]

+/** True when the text holds something a reader would actually see. */
+function hasVisibleText(value?: string | null) {
+  return !!value?.replace(ZERO_WIDTH, "").trim();
+}
Relevance

●●● Strong

Recent repository reviews accepted explicit return annotations for new TypeScript helpers and public
functions.

PR-#1531
PR-#1528

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 requires explicit type annotations in modified TypeScript code and
disallows implicit typing; the newly added hasVisibleText function omits an explicit return type
(it relies on inference).

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[99-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`hasVisibleText` was introduced without an explicit return type, which violates the project rule disallowing implicit types in new/modified TypeScript code.
## Issue Context
The helper is newly added in this PR and should be explicitly typed to prevent accidental widening or drift.
## Fix Focus Areas
- apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[99-102]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. hasVisibleText missing return type ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new helper hasVisibleText has no explicit return type annotation, relying on inference. This
violates the rule prohibiting implicit types in newly added/modified TypeScript code and can allow
type drift if the implementation changes.
Code

apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[R99-102]

+/** True when the text holds something a reader would actually see. */
+function hasVisibleText(value?: string | null) {
+  return !!value?.replace(ZERO_WIDTH, "").trim();
+}
Relevance

●●● Strong

Recent repository reviews accepted explicit return annotations for new TypeScript helpers and public
functions.

PR-#1531
PR-#1528

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 requires explicit type annotations in modified TypeScript code and
disallows implicit typing; the newly added hasVisibleText function omits an explicit return type
(it relies on inference).

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[99-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`hasVisibleText` was introduced without an explicit return type, which violates the project rule disallowing implicit types in new/modified TypeScript code.
## Issue Context
The helper is newly added in this PR and should be explicitly typed to prevent accidental widening or drift.
## Fix Focus Areas
- apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[99-102]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. hasVisibleText missing return type ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new helper hasVisibleText has no explicit return type annotation, relying on inference. This
violates the rule prohibiting implicit types in newly added/modified TypeScript code and can allow
type drift if the implementation changes.
Code

apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[R99-102]

+/** True when the text holds something a reader would actually see. */
+function hasVisibleText(value?: string | null) {
+  return !!value?.replace(ZERO_WIDTH, "").trim();
+}
Relevance

●●● Strong

Recent repository reviews accepted explicit return annotations for new TypeScript helpers and public
functions.

PR-#1531
PR-#1528

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 requires explicit type annotations in modified TypeScript code and
disallows implicit typing; the newly added hasVisibleText function omits an explicit return type
(it relies on inference).

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[99-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`hasVisibleText` was introduced without an explicit return type, which violates the project rule disallowing implicit types in new/modified TypeScript code.
## Issue Context
The helper is newly added in this PR and should be explicitly typed to prevent accidental widening or drift.
## Fix Focus Areas
- apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[99-102]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (3)
4. vi.mock internal module used ✓ Resolved 📘 Rule violation ▣ Testability
Description
The new spec file mocks an internal module via vi.mock(...), which violates the constraint that
unit tests should mock only external package dependencies with Vitest mocks. This increases test
coupling to internal module structure and can hide integration issues.
Code

apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[R7-10]

+vi.mock("@/features/tiptap-editor/extensions", async () => ({
+  ...(await vi.importActual("@/features/tiptap-editor/extensions")),
+  HIVE_POST_PURE_REGEX: /$a^/
+}));
Evidence
PR Compliance ID 2668008 restricts Vitest mocks to external package dependencies; the test uses
vi.mock on an internal alias import (@/features/tiptap-editor/extensions).

Rule 2668008: Mock only external package dependencies with vi.fn in unit tests
apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[7-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test mocks an internal module (`@/features/tiptap-editor/extensions`) using `vi.mock`, but the rule allows Vitest mocking only for external package dependencies.
## Issue Context
This mock overrides `HIVE_POST_PURE_REGEX` to work around jsdom limitations.
## Fix Focus Areas
- apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[7-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. vi.mock internal module used ✓ Resolved 📘 Rule violation ▣ Testability
Description
The new spec file mocks an internal module via vi.mock(...), which violates the constraint that
unit tests should mock only external package dependencies with Vitest mocks. This increases test
coupling to internal module structure and can hide integration issues.
Code

apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[R7-10]

+vi.mock("@/features/tiptap-editor/extensions", async () => ({
+  ...(await vi.importActual("@/features/tiptap-editor/extensions")),
+  HIVE_POST_PURE_REGEX: /$a^/
+}));
Evidence
PR Compliance ID 2668008 restricts Vitest mocks to external package dependencies; the test uses
vi.mock on an internal alias import (@/features/tiptap-editor/extensions).

Rule 2668008: Mock only external package dependencies with vi.fn in unit tests
apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[7-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test mocks an internal module (`@/features/tiptap-editor/extensions`) using `vi.mock`, but the rule allows Vitest mocking only for external package dependencies.
## Issue Context
This mock overrides `HIVE_POST_PURE_REGEX` to work around jsdom limitations.
## Fix Focus Areas
- apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[7-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. vi.mock internal module used ✓ Resolved 📘 Rule violation ▣ Testability
Description
The new spec file mocks an internal module via vi.mock(...), which violates the constraint that
unit tests should mock only external package dependencies with Vitest mocks. This increases test
coupling to internal module structure and can hide integration issues.
Code

apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[R7-10]

+vi.mock("@/features/tiptap-editor/extensions", async () => ({
+  ...(await vi.importActual("@/features/tiptap-editor/extensions")),
+  HIVE_POST_PURE_REGEX: /$a^/
+}));
Evidence
PR Compliance ID 2668008 restricts Vitest mocks to external package dependencies; the test uses
vi.mock on an internal alias import (@/features/tiptap-editor/extensions).

Rule 2668008: Mock only external package dependencies with vi.fn in unit tests
apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[7-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test mocks an internal module (`@/features/tiptap-editor/extensions`) using `vi.mock`, but the rule allows Vitest mocking only for external package dependencies.
## Issue Context
This mock overrides `HIVE_POST_PURE_REGEX` to work around jsdom limitations.
## Fix Focus Areas
- apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[7-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 7 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a6d72802-6d16-41b1-af5c-247c28e31528

📥 Commits

Reviewing files that changed from the base of the PR and between aa57c18 and 62d6a56.

📒 Files selected for processing (9)
  • apps/web/src/app/publish/_components/publish-editor-toolbar-fragments.tsx
  • apps/web/src/app/publish/_components/publish-translate-dialog.tsx
  • apps/web/src/features/tiptap-editor/functions/index.ts
  • apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts
  • apps/web/src/features/tiptap-editor/functions/resolve-insert-content.ts
  • apps/web/src/features/tiptap-editor/plugins/clipboard/clipboard-plugin-text-strategy.ts
  • apps/web/src/specs/features/tiptap-editor/empty-list-item-paste.spec.ts
  • apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts
  • apps/web/src/specs/features/tiptap-editor/publish-editor-extensions.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

qodo-code-review Bot commented Aug 24, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Fix TipTap paste/insert: avoid literal markup for unrenderable or text-only content

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Prevent TipTap from inserting unrenderable HTML as visible escaped markup on paste/insert.
• Insert decoded plain text for text-only parses to avoid double-escaped entities.
• Treat zero-width format characters as invisible in paste normalisation and add regression specs.
Diagram

graph TD
  A(["Clipboard paste"]) --> B["simpleMarkdownToHTML"] --> C["parseAllExtensionsToDoc"] --> D["resolveInsertContent"] --> E["editor.insertContent"] --> I[("ProseMirror doc")]
  F(["Toolbar fragments"]) --> B
  G(["Translate dialog"]) --> B
  H[("TipTap schema")] --> D

  subgraph Legend
    direction LR
    _ui(["UI entrypoint"]) ~~~ _fn["Function"] ~~~ _db[("Doc/Schema")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Patch/upgrade TipTap insertContentAt behavior
  • ➕ Fixes the root cause once for all insertContent callers and downstream consumers
  • ➕ Reduces need for local workarounds and custom invariants
  • ➖ May require waiting for upstream change or carrying a fork/patch
  • ➖ Behavior change risk across the app if TipTap internals differ by version
2. Custom insert command that directly calls tr.insertText / replaceSelection
  • ➕ Avoids relying on TipTap's insertContentAt heuristics entirely
  • ➕ Can precisely control empty-slice and text-only handling
  • ➖ More ProseMirror-specific code to maintain
  • ➖ Harder to keep parity with TipTap features (marks, selection behavior, etc.)

Recommendation: The current approach (a small, schema-aware resolveInsertContent helper used at all insertContent call-sites) is the best local mitigation: it contains the workaround, preserves existing HTML insertion for renderable content, and adds regression coverage for the empty-slice and entity-decoding cases. Upstreaming or upgrading TipTap can still be pursued later, but this keeps user-facing paste behavior correct now.

Files changed (9) +226 / -23

Bug fix (5) +105 / -22
publish-editor-toolbar-fragments.tsxUse resolveInsertContent before inserting toolbar fragments +8/-6

Use resolveInsertContent before inserting toolbar fragments

• Routes fragment insertion through resolveInsertContent so schema-unrenderable content doesn't get inserted as escaped literal markup. Skips insertion entirely when the resolver returns null.

apps/web/src/app/publish/_components/publish-editor-toolbar-fragments.tsx

publish-translate-dialog.tsxResolve translated appendix content before insertContent +12/-6

Resolve translated appendix content before insertContent

• Uses resolveInsertContent for the translated appendix insertion path. Prevents empty/unrenderable parses from leaking normalised HTML as visible text.

apps/web/src/app/publish/_components/publish-translate-dialog.tsx

parse-all-extensions-to-doc.tsTreat zero-width characters as invisible in paste normalisation +18/-5

Treat zero-width characters as invisible in paste normalisation

• Introduces a hasVisibleText helper that strips common zero-width format characters before trim-based checks. Applies it across unwrap/emptiness logic to avoid blank-looking list items being treated as content.

apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts

resolve-insert-content.tsAdd schema-aware insertContent resolver for empty/text-only parses +57/-0

Add schema-aware insertContent resolver for empty/text-only parses

• Adds resolveInsertContent to decide whether to insert HTML, decoded plain text, or a fallback clipboard string when the schema renders nothing. Prevents TipTap's empty-slice path from inserting HTML strings as literal text and avoids double-escaped entity insertion.

apps/web/src/features/tiptap-editor/functions/resolve-insert-content.ts

clipboard-plugin-text-strategy.tsApply resolveInsertContent to text-clipboard paste strategy +10/-5

Apply resolveInsertContent to text-clipboard paste strategy

• Stops inserting parseAllExtensionsToDoc output directly, instead resolving a safe insert payload with fallback to the user's clipboard text. Avoids literal escaped markup when the parse result is empty.

apps/web/src/features/tiptap-editor/plugins/clipboard/clipboard-plugin-text-strategy.ts

Refactor (1) +1 / -0
index.tsExport resolveInsertContent from tiptap-editor functions +1/-0

Export resolveInsertContent from tiptap-editor functions

• Re-exports the new resolveInsertContent helper so it can be shared across editor entrypoints.

apps/web/src/features/tiptap-editor/functions/index.ts

Tests (2) +109 / -1
empty-list-item-paste.spec.tsAdd zero-width regression cases for blank list items +6/-1

Add zero-width regression cases for blank list items

• Extends the blank-item paste matrix to cover U+200B, U+FEFF, and U+200D cases that previously produced blank-looking bullets. Ensures these are normalised to a single empty paragraph.

apps/web/src/specs/features/tiptap-editor/empty-list-item-paste.spec.ts

paste-renders-as-nothing.spec.tsAdd regression spec for unrenderable paste leaking literal markup +103/-0

Add regression spec for unrenderable paste leaking literal markup

• Adds a focused spec covering the empty-slice TipTap insertion leak (iframe/video) and the entity double-escape behavior for text-only parses. Includes a jsdom-safe mock for the hive-post innerText-dependent pass.

apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts

Documentation (1) +11 / -0
publish-editor-extensions.tsDocument jsdom limitations affecting paste-normalisation specs +11/-0

Document jsdom limitations affecting paste-normalisation specs

• Documents two known jsdom vs Chromium gaps (missing innerText and foster parenting differences in tables) and clarifies why some behaviors must be verified in a real browser.

apps/web/src/specs/features/tiptap-editor/publish-editor-extensions.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e21da726fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let node: ChildNode | null = child.previousSibling;
while (node) {
if (node.textContent?.trim()) {
if (hasVisibleText(node.textContent)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove ignored zero-width nodes before prepending

When a list item has only a zero-width text node before a leading block—for example, the tight nested-list markdown - \u200b\n - nested—this now reports no preceding text and prepends an empty <p>, but leaves the zero-width node in place. ProseMirror still wraps that surviving inline node in its own paragraph, so the item acquires both a zero-width paragraph and the newly inserted empty paragraph, producing an extra blank line. Either remove the ignored nodes before prepending or continue treating them as satisfying the leading paragraph requirement.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in cf1f5dd600.

Reproduced it first rather than taking the reasoning: <ul><li>\u200B<ul><li>x</li></ul></li></ul> rendered as <li><p>\u200B</p><p></p><ul>…, two paragraphs, exactly as you describe. Same for the tight markdown form.

Took your first option. Anything text-like ahead of the block is invisible by definition at that point, since hasTextBefore just said so, and dropping it is consistent with treating zero-width characters as invisible everywhere else in the file. Keeping them as satisfying the requirement would have meant the leading paragraph was a zero-width one, which renders as a blank line just the same.

It also fixes the whitespace and newline forms, which had the same shape before the zero-width change and were producing the extra paragraph already: <li>\n<ul>… now yields one empty paragraph rather than two. Four cases added, 3 of which fail without the fix.

Review: treating zero-width characters as invisible made hasTextBefore report no
lead-in, so a paragraph was prepended while the zero-width text node survived.
ProseMirror wraps that in a paragraph of its own, so the item ended up with two
and rendered a blank line. Anything text-like ahead of the block is invisible by
definition at that point, so remove it.

3 of the 4 new cases fail without this.
@qodo-code-review

qodo-code-review Bot commented Aug 24, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. hasVisibleText missing return type ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new helper hasVisibleText has no explicit return type annotation, relying on inference. This
violates the rule prohibiting implicit types in newly added/modified TypeScript code and can allow
type drift if the implementation changes.
Code

apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[R99-102]

+/** True when the text holds something a reader would actually see. */
+function hasVisibleText(value?: string | null) {
+  return !!value?.replace(ZERO_WIDTH, "").trim();
+}
Relevance

●●● Strong

Recent repository reviews accepted explicit return annotations for new TypeScript helpers and public
functions.

PR-#1531
PR-#1528

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 requires explicit type annotations in modified TypeScript code and
disallows implicit typing; the newly added hasVisibleText function omits an explicit return type
(it relies on inference).

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[99-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`hasVisibleText` was introduced without an explicit return type, which violates the project rule disallowing implicit types in new/modified TypeScript code.

## Issue Context
The helper is newly added in this PR and should be explicitly typed to prevent accidental widening or drift.

## Fix Focus Areas
- apps/web/src/features/tiptap-editor/functions/parse-all-extensions-to-doc.ts[99-102]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. vi.mock internal module used ✓ Resolved 📘 Rule violation ▣ Testability
Description
The new spec file mocks an internal module via vi.mock(...), which violates the constraint that
unit tests should mock only external package dependencies with Vitest mocks. This increases test
coupling to internal module structure and can hide integration issues.
Code

apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[R7-10]

+vi.mock("@/features/tiptap-editor/extensions", async () => ({
+  ...(await vi.importActual("@/features/tiptap-editor/extensions")),
+  HIVE_POST_PURE_REGEX: /$a^/
+}));
Evidence
PR Compliance ID 2668008 restricts Vitest mocks to external package dependencies; the test uses
vi.mock on an internal alias import (@/features/tiptap-editor/extensions).

Rule 2668008: Mock only external package dependencies with vi.fn in unit tests
apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[7-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test mocks an internal module (`@/features/tiptap-editor/extensions`) using `vi.mock`, but the rule allows Vitest mocking only for external package dependencies.

## Issue Context
This mock overrides `HIVE_POST_PURE_REGEX` to work around jsdom limitations.

## Fix Focus Areas
- apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts[7-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 84 rules
✅ Skills: 6 invoked
  add-feature
  add-query
  add-sdk-mutation
  add-test
  code-review
  debug
✅ Web pages:
  +14 more
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread apps/web/src/specs/features/tiptap-editor/paste-renders-as-nothing.spec.ts Outdated
feruzm added 2 commits August 24, 2026 11:30
…c mock

Two review rule violations. The helpers in parse-all-extensions-to-doc relied on
inferred return types; they are annotated now, including the ones that predate
this PR so the file is consistent.

The new spec mocked the extensions barrel to stub the hive-post regex, which is
the jsdom innerText workaround. None of its inputs contains an <a href>, so that
filter never runs and the mock was never needed. Removed, so the spec runs the
real module.
…d nothing

Review: removing only text nodes left a skipped wrapper in place, so
<li><span>​</span><ul>...</ul></li> still got two paragraphs, the wrapper's
invisible text in one and the inserted empty one after it.

The suggested rule was that every sibling skipped while locating the block is
non-renderable, and that is not quite true. The walk also skipped a wrapper that
renders nothing ITSELF but contains something: <span><img></span> was stepped
over, so removing every skipped sibling would have deleted the image.

So the walk now stops at anything holding renderable content, which makes the
rule true, and everything ahead of the block is removed. That also drops a
spurious paragraph the image case was getting: it satisfies the leading
paragraph on its own, so none is added.

3 of the 4 new cases fail without this.
@feruzm

feruzm commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

Fixed in 62d6a56. Confirmed the finding first: <li>​<span></span><ul>…</ul></li> really did keep the wrapper and render <p>​</p><p></p><ul>…, two paragraphs.

One correction to the suggested rule, which is why the fix is not a one-liner. "Every sibling skipped while finding first is non-renderable" does not hold. The walk also steps over a wrapper that renders nothing itself but contains something:

<li><span><img src="…"></span><ul><li>x</li></ul></li>

The <span> matches nothing in the schema and has no text, so it was skipped, and removing every skipped sibling would have deleted the image with it.

So the walk now stops at anything holding renderable content, which makes the rule true, and then everything ahead of the block is removed. That also fixes a spurious paragraph the image case was already getting on develop: the image satisfies the leading-paragraph requirement on its own, so none is added.

input before after
<span>​</span> then list <p>​</p><p></p><ul> <p></p><ul>
<span></span><u>​</u> then list <p>​</p><p></p><ul> <p></p><ul>
<span><img></span> then list <p><img></p><p></p><ul> <p><img></p><ul>

Four cases added, 3 fail without the fix. Full web suite 3551, tsc and lint clean.

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.

Publish editor: a paste that renders as nothing is inserted as literal HTML text

1 participant