Skip to content

fix: orphan ZWS after linebreak deletes whole line on Backspace#71

Open
Karol-Stelmaczonek wants to merge 3 commits into
developfrom
fix/INF-530-orphan-zws-after-linebreak
Open

fix: orphan ZWS after linebreak deletes whole line on Backspace#71
Karol-Stelmaczonek wants to merge 3 commits into
developfrom
fix/INF-530-orphan-zws-after-linebreak

Conversation

@Karol-Stelmaczonek

@Karol-Stelmaczonek Karol-Stelmaczonek commented Jul 21, 2026

Copy link
Copy Markdown

Ticket : https://oat-sa.atlassian.net/browse/INF-530

Summary

  • Fixes INF-530: after applying ruby, Enter, then removing ruby, Backspace near the orphan ZWS no longer deletes the whole line.
  • Improves taofurigana Backspace/Delete handling around ruby (orphan ZWS cleanup, empty rt unwrap, caret placement).
  • Adds scripts/link-to-tao.sh for syncing a built CKEditor into tao-core shared-libs (paths must be configured; placeholders fail loudly).

Test plan

  • Reproduce INF-530 steps in qtiCreator with a built+synced CKEditor: apply ruby at end of line → Enter → remove ruby → Backspace near the break — only one character/ZWS should be removed, not the whole line
  • Apply/remove ruby, Backspace into rt / after ruby — caret and deletion behave as expected
  • Empty rt unwraps cleanly without sluggish thrashing
  • ./scripts/link-to-tao.sh status without configured paths prints setup help and exits non-zero

Video

Screen.Recording.2026-07-21.at.11.45.28.mov

Summary by CodeRabbit

Summary by CodeRabbit

  • Bug Fixes

    • Improved furigana/ruby editing reliability, including more consistent Backspace/Delete behavior, caret handling, and removal of leftover anchor artifacts.
    • Refined command/UI state synchronization so toolbar availability updates more reliably.
  • New Features

    • Added a local CKEditor sync/restore/status workflow to support qtiCreator testing with unminified builds.
  • Documentation

    • Expanded CK Playground instructions with furigana/ruby guidance and updated qtiCreator testing steps.
  • Chores

    • Updated the furigana plugin’s English language registration.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The plugin updates ruby cleanup, caret handling, command-state scheduling, and editor event coordination. A new link-to-tao.sh utility supports qtiCreator bundle syncing and restoration, with README instructions and corrected English language registration.

Changes

Furigana and qtiCreator workflow

Layer / File(s) Summary
Ruby lifecycle and orphan anchor handling
plugins/taofurigana/plugin.js, plugins/taofurigana/lang/en.js
Ruby elements use shared unwrapping helpers, caret and zero-width-space anchors are normalized, orphan anchors are cleaned across boundaries, and English strings register under taofurigana.
Command state and editor event coordination
plugins/taofurigana/plugin.js
Command refreshes and toolbar disabling are scheduled, while keyboard, selection, change, data-ready, mouse, and blur events coordinate ruby cleanup and UI state updates.
qtiCreator bundle synchronization workflow
scripts/link-to-tao.sh, README.md
The script validates paths and builds, supports sync, restore, and status, and the README documents furigana development and qtiCreator testing workflows.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant TaofuriganaPlugin
  participant RubyDom
  participant Toolbar
  Editor->>TaofuriganaPlugin: Trigger keyboard or selection event
  TaofuriganaPlugin->>RubyDom: Normalize ruby content and anchors
  TaofuriganaPlugin->>Toolbar: Refresh command and button state
  TaofuriganaPlugin->>Editor: Apply caret, selection, or change result
Loading

Suggested reviewers: tikhanovicha, bartlomiejmarszal, kirylhatalski, pnal

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main bug fix around orphan ZWS and Backspace deleting the whole line.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/INF-530-orphan-zws-after-linebreak

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

@Karol-Stelmaczonek
Karol-Stelmaczonek requested review from a team, KirylHatalski, bartlomiejmarszal, pnal and tikhanovichA and removed request for a team July 21, 2026 10:43

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/taofurigana/plugin.js`:
- Around line 369-377: Update unwrapRubyToBaseText around rbElement and
replacement so unwrapping preserves inline markup contained in rb instead of
flattening it through rbElement.getText(). Reuse the existing HTML-preserving
createFromHtml approach from the blur cleanup path, while retaining the current
empty-rb handling and replacement/cleanup flow.

In `@README.md`:
- Around line 48-51: Update the qtiCreator testing workflow documentation to add
a configuration step before the build command, instructing users to set
CKEDITOR_DEV and TAO_CORE_VIEWS via exports or the appropriate configuration
edit. Keep the existing build, sync, refresh, and restore steps unchanged.

In `@scripts/link-to-tao.sh`:
- Line 124: Update the size calculation in cmd_status so a missing ckeditor.js
does not cause the command to exit under set -euo pipefail. Handle the failed
wc/input-redirection operation before assigning size, while preserving the
existing ${size:-?} fallback for absent files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8c2c3877-f904-4c4a-a62e-072018ca4449

📥 Commits

Reviewing files that changed from the base of the PR and between bb7c58c and 64fcdfb.

📒 Files selected for processing (4)
  • README.md
  • plugins/taofurigana/lang/en.js
  • plugins/taofurigana/plugin.js
  • scripts/link-to-tao.sh

Comment thread plugins/taofurigana/plugin.js
Comment thread README.md Outdated
Comment thread scripts/link-to-tao.sh Outdated
Preserve inline markup when unwrapping ruby, document path setup before sync, and harden status when ckeditor.js is missing.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
plugins/taofurigana/plugin.js (1)

371-391: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve every rb child when unwrapping.

createFromHtml() yields one node. For mixed base content such as 漢<em>字</em>, replacing the ruby with that single node drops remaining siblings. Replace the ruby through a fragment/all parsed children and place the caret after the final inserted node.

#!/bin/bash
set -euo pipefail

fd -a '^element\.js$' core | while read -r file; do
  rg -n -C8 'createFromHtml' "$file"
done

rg -n -C4 'createFromHtml\(rbInnerHtml|replacement\.replace\(rubyElement\)' \
  plugins/taofurigana/plugin.js
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/taofurigana/plugin.js` around lines 371 - 391, Update the
ruby-unwrapping logic around replacement and replacement.replace(rubyElement) to
parse rbInnerHtml as a fragment or otherwise collect all parsed child nodes,
preserving mixed text and inline markup such as 漢<em>字</em> instead of retaining
only the first node. Insert every collected node in order, and position the
caret after the final inserted node; retain the existing text fallback when
parsing fails or produces no usable children.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/link-to-tao.sh`:
- Around line 123-127: Update the ckeditor.js handling around the ckeditor_js
and size variables to add an explicit missing-file branch: print an error when
the file does not exist and return a non-zero status. Preserve the existing size
calculation and successful flow when ckeditor.js is present, preventing the
later Active copy message from reporting an empty size.

---

Duplicate comments:
In `@plugins/taofurigana/plugin.js`:
- Around line 371-391: Update the ruby-unwrapping logic around replacement and
replacement.replace(rubyElement) to parse rbInnerHtml as a fragment or otherwise
collect all parsed child nodes, preserving mixed text and inline markup such as
漢<em>字</em> instead of retaining only the first node. Insert every collected
node in order, and position the caret after the final inserted node; retain the
existing text fallback when parsing fails or produces no usable children.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 218336ec-c703-48cb-a919-8a7972a949c7

📥 Commits

Reviewing files that changed from the base of the PR and between 64fcdfb and 0d9c603.

📒 Files selected for processing (3)
  • README.md
  • plugins/taofurigana/plugin.js
  • scripts/link-to-tao.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

Comment thread scripts/link-to-tao.sh Outdated
… missing

Parse all rb child nodes when unwrapping ruby, and make link-to-tao status exit non-zero when ckeditor.js is absent.
@Karol-Stelmaczonek Karol-Stelmaczonek changed the title fix(INF-530): orphan ZWS after linebreak deletes whole line on Backspace fix: orphan ZWS after linebreak deletes whole line on Backspace Jul 21, 2026
var rtElement = startNode.getAscendant('rt', true);

// Do not re-seed ZWS into an empty rt — that causes Backspace to thrash
// textLen 0↔1 forever (see INF-530 probe). New rubies get anchors at create time.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Plese remove ticket reference from comment

var node = getNextNodeCrossingBoundaries(fromNode);
var guard = 0;

while (node && guard < 50) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why 50? Should we use const for it?

Comment on lines +673 to +688
if (isOrphanZwsAnchor(node)) {
cleanupZwsAnchor(node);
return true;
}

if (isEmptyTextNode(node)) {
node = getNextNodeCrossingBoundaries(node);
continue;
}

if (node.type === CKEDITOR.NODE_ELEMENT) {
var name = node.getName && node.getName();
if (name === 'br') {
node = getNextNodeCrossingBoundaries(node);
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we comment each condition so we know what they are executing and why?

}

/**
* Chrome can delete a whole line when Backspace hits an orphan ZWS left after

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is it only chrome?

Comment on lines +720 to +727
if (keyCode !== keyCodeBackspace || !selection || !selection.isCollapsed()) {
return false;
}

var range = selection.getRanges()[0];
if (!range || !range.startContainer) {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we describe this conditions to understand why they are executed?

/**
* Change command state according to the current selection content
* Change command state according to the current selection content.
* Debounced: uncanceled setTimeout(150) on every keyup was stacking and making

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DO we really need historical code state in this comment?

statelessButtons.forEach(function (button) {
button.setState(CKEDITOR.TRISTATE_OFF);
});
}, 150);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we extract this value (150) into Const?

Comment thread scripts/link-to-tao.sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why do we need this script here?

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.

3 participants