Skip to content

Posting-3: Tweet-splitter#138

Open
mlv-dev wants to merge 20 commits intoposting.finalfrom
posting.3-tweet_splitter
Open

Posting-3: Tweet-splitter#138
mlv-dev wants to merge 20 commits intoposting.finalfrom
posting.3-tweet_splitter

Conversation

@mlv-dev
Copy link
Copy Markdown
Collaborator

@mlv-dev mlv-dev commented Aug 27, 2025

Summary by CodeRabbit

  • New Features

    • Added a tweet-splitting tool in the editor with a “Разбить на твиты” button.
    • Inserts visual separators, per‑tweet numbering, arrows, and live character counters.
    • Validates a 280‑character limit per tweet (URLs treated as 23 chars, emoji as 2) and highlights over‑limit tweets.
    • Copying content omits separators and counters, preserving plain text.
  • Style

    • Added styling for separators, counters, numbering badges, arrows, and remove controls.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 27, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbit review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds a new CKEditor plugin "tweet_splitter" and accompanying stylesheet that inserts tweet-separators, computes Twitter-style per-tweet lengths (URLs/emoji rules), displays counters/numbering/arrows, customizes copy behavior, and validates tweets against a 280-character limit.

Changes

Cohort / File(s) Summary of Changes
CKEditor Tweet Splitter Plugin (JS)
static/ckeditor/ckeditor/plugins/tweet_splitter/plugin.js
New CKEditor plugin tweet_splitter: registers command insertTweetSplitter and UI button “Разбить на твиты”; defines TWEET_LIMIT, twitterLength, validateTweets, updateTweetCounters, and scheduleTweetCountersUpdate; inserts non-editable separator blocks with remove action; attaches event hooks (instanceReady, key, afterCommandExec, contentDom) to maintain counters; custom clipboard copy handling to strip UI elements; form-submit validation.
Tweet Splitter Styles (CSS)
static/ckeditor/ckeditor/plugins/tweet_splitter/styles.css
New stylesheet adding selectors and styles for .tweet-numbering, .tweet-arrow, .tweet-char-counter, .tweet-separator, .tweet-separator-line, .tweet-separator-text, .tweet-separator-remove (including hover and focus-visible) and SVG sizing; uses grayscale palette and makes interactive elements non-selectable.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant E as CKEditor
  participant P as tweet_splitter Plugin
  participant D as Editor DOM
  participant F as Form

  U->>E: Click "Разбить на твиты"
  E->>P: exec insertTweetSplitter
  P->>D: Insert separator block (label + remove)
  P->>P: updateTweetCounters()
  P->>D: Render counters/numbering/arrows

  rect rgba(230,240,255,0.2)
    U-->>E: Type / paste / commands
    E->>P: key / afterCommandExec / contentDom events
    P->>P: scheduleTweetCountersUpdate()
    P->>D: Recompute lengths & update UI
  end

  U->>F: Submit form
  F->>P: validateTweets(editor)
  alt Any tweet > 280 chars
    P-->>F: Validation fails
    F-->>U: Block submit / show message
  else All tweets within limit
    F-->>U: Submit proceeds
  end

  opt Copy selection
    U->>E: Copy
    E->>P: Clipboard hook
    P->>P: Strip separators/counters, return plain text
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch posting.3-tweet_splitter

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@mlv-dev
Copy link
Copy Markdown
Collaborator Author

mlv-dev commented Aug 27, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 27, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mlv-dev mlv-dev changed the title Posting-3: Tweet-spiltter Posting-3: Tweet-splitter Aug 27, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (10)
static/ckeditor/ckeditor/plugins/tweet_splitter/styles.css (3)

1-4: Scope CSS: avoid overriding the editor’s global typography.

Remove the global body rule to prevent unexpected font/line-height changes to all editor content. Scope styles to plugin UI only.

-body {
-    font-family: 'Helvetica', sans-serif;
-    line-height: 1.6;
-}

61-73: Add visible focus styles for keyboard accessibility.

Make the remove control clearly focusable, not just hoverable.

 .tweet-separator-remove:hover {
     background-color: #ddd;
 }
+
+.tweet-separator-remove:focus-visible {
+    outline: 2px solid #666;
+    outline-offset: 2px;
+    border-radius: 4px;
+}

61-69: Increase hit area for the remove control.

Small targets hurt usability. Add minimal padding.

 .tweet-separator-remove {
     cursor: pointer;
     display: inline-flex;
     align-items: center;
     line-height: 1;
     user-select: none;
     vertical-align: middle;
     margin-left: 5px;
+    padding: 2px;
+    border-radius: 4px;
 }
static/ckeditor/ckeditor/plugins/tweet_splitter/plugin.js (7)

61-61: Remove stray double semicolon.

-                let html = editor.getSelectedHtml(true);;
+                let html = editor.getSelectedHtml(true);

57-73: Harden copy handler for broader browser support.

Guard clipboardData and provide a plain-text fallback to avoid exceptions in constrained environments.

-            editor.document.on('copy', function(evt) {
+            editor.document.on('copy', function(evt) {
                 const e = evt.data.$; // нативный ClipboardEvent
-                e.preventDefault(); // отменяем стандартное копирование
+                e.preventDefault(); // отменяем стандартное копирование
 
                 let html = editor.getSelectedHtml(true);
 
                 // Удаляем div tweet-separator и tweet-char-counter целиком
                 html = html.replace(/<div class="tweet-(separator|char-counter)[^>]*>[\s\S]*?<\/div>/g, '');
 
                 // Убираем все теги, оставляем только текст
                 let text = html.replace(/<[^>]+>/g, '');
 
                 // Кладём своё
-                e.clipboardData.setData('text/plain', text);
-                e.clipboardData.setData('text/html', text);
+                if (e.clipboardData && e.clipboardData.setData) {
+                    e.clipboardData.setData('text/plain', text);
+                    e.clipboardData.setData('text/html', text);
+                } else if (window.clipboardData && window.clipboardData.setData) {
+                    // IE fallback
+                    window.clipboardData.setData('Text', text);
+                }
             });

12-17: Make the remove control accessible.

Add button semantics and keyboard operability hints.

-                            <span class="tweet-separator-remove" title="Объединить твиты">
+                            <span class="tweet-separator-remove" title="Объединить твиты" role="button" tabindex="0" aria-label="Объединить твиты">

24-35: Use event delegation for remove to cover future separators and avoid re-binding.

One listener on the editor document is cheaper and more reliable than per-node onclick.

-                const editorDom = editor.document.$;
-                editorDom.querySelectorAll('.tweet-separator-remove').forEach(button => {
-                    button.onclick = function() {
-                        const separator = button.closest('.tweet-separator');
-                        if (separator) {
-                            separator.remove();
-                            setTimeout(() => updateTweetCounters(editor), 100);
-                            editor.focus();
-                        }
-                    };
-                });
+                const nativeDoc = editor.document.$;
+                if (!nativeDoc._tweetSplitterRemoveBound) {
+                    nativeDoc._tweetSplitterRemoveBound = true;
+                    nativeDoc.addEventListener('click', (ev) => {
+                        const btn = ev.target.closest && ev.target.closest('.tweet-separator-remove');
+                        if (!btn) return;
+                        const sep = btn.closest('.tweet-separator');
+                        if (sep) {
+                            sep.remove();
+                            scheduleTweetCountersUpdate(editor);
+                            editor.focus();
+                        }
+                    });
+                    nativeDoc.addEventListener('keydown', (ev) => {
+                        if ((ev.key === 'Enter' || ev.key === ' ') && ev.target.classList && ev.target.classList.contains('tweet-separator-remove')) {
+                            ev.preventDefault();
+                            ev.target.click();
+                        }
+                    });
+                }

84-93: Emoji/URL counting: verify browser targets or use twitter-text for spec-accurate length.

Unicode property escapes may not be supported in legacy browsers and will throw at parse-time. Consider a guarded fallback or adopting twitter-text’s parsing for correctness.

Guarded fallback:

-    // Эмодзи считаем за 2 символа
-    const emojiRegex = /[\p{Emoji_Presentation}\p{Emoji}\u200d]/gu;
-    text = text.replace(emojiRegex, 'xx');
+    // Эмодзи считаем за 2 символа (guard for browsers without Unicode property escapes)
+    try {
+        const emojiRegex = /[\p{Emoji_Presentation}\p{Emoji}\u200d]/gu;
+        text = text.replace(emojiRegex, 'xx');
+    } catch (_) {
+        // Fallback: approximate by replacing surrogate pairs and ZWJ
+        text = text
+            .replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 'xx')
+            .replace(/\u200d/g, 'x'); // keep ZWJ light to avoid double counting
+    }

Option: integrate twitter-text and delegate length calculations for exact parity with Twitter.


2-2: Icon config mismatch.

You set icons: 'tweet_splitter' but the button uses icon: 'horizontalrule'. Either add an icon asset or drop the icons property to avoid 404s.

-    icons: 'tweet_splitter',
+    // icons: 'tweet_splitter', // not needed since button uses built-in icon

Also applies to: 38-43


95-107: Bind submit validation to the editor’s owning form, not all forms.

Current code attaches to every form on the page and validates all editor instances, which can block unrelated forms. Scope to the editor’s parent form and validate that instance.

Example (move into init after instanceReady):

-document.querySelectorAll("form").forEach(form => {
-        form.addEventListener("submit", function(e) {
-            // Берём первый доступный CKEditor (или пробегаем все, если их несколько)
-            for (let instanceName in CKEDITOR.instances) {
-                const editor = CKEDITOR.instances[instanceName];
-                if (!validateTweets(editor)) {
-                    e.preventDefault(); // блокируем отправку
-                    alert("❌ Нельзя сохранить: один или несколько твитов превышают 280 символов!");
-                    return false; // выходим из цикла, форма не отправляется
-                }
-            }
-        });
-    });
+const container = editor.container && editor.container.$;
+const form = container && container.closest ? container.closest('form') : null;
+if (form && !form._tweetSplitterBound) {
+    form._tweetSplitterBound = true;
+    form.addEventListener('submit', (e) => {
+        if (!validateTweets(editor)) {
+            e.preventDefault();
+            alert("❌ Нельзя сохранить: один или несколько твитов превышают 280 символов!");
+        }
+    });
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cb7a71d and 732dfe4.

📒 Files selected for processing (2)
  • static/ckeditor/ckeditor/plugins/tweet_splitter/plugin.js (1 hunks)
  • static/ckeditor/ckeditor/plugins/tweet_splitter/styles.css (1 hunks)

Comment thread static/ckeditor/ckeditor/plugins/tweet_splitter/plugin.js
Comment thread static/ckeditor/ckeditor/plugins/tweet_splitter/plugin.js Outdated
@mlv-dev mlv-dev changed the base branch from master to posting.final August 27, 2025 15:52
@mlv-dev
Copy link
Copy Markdown
Collaborator Author

mlv-dev commented Aug 27, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 27, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (7)
static/ckeditor/ckeditor/plugins/tweet_splitter/styles.css (2)

46-54: Boost contrast/legibility of “Новый твит” label (WCAG).

12px uppercase at #999 risks failing contrast for small text. Slightly darken or bump size.

 .tweet-separator-text {
-    padding: 0 15px;
-    color: #999;
-    font-size: 12px;
+    padding: 0 15px;
+    color: #666; /* better contrast */
+    font-size: 13px;
     font-weight: bold;
     text-transform: uppercase;
     display: flex;
     align-items: center;
 }

56-66: Increase hit target and add active state for the remove control.

Improve usability and keyboard/mouse affordance.

 .tweet-separator-remove {
     cursor: pointer;
     display: inline-flex;
     align-items: center;
     line-height: 1;
     user-select: none;
     vertical-align: middle;
-    margin-left: 5px;
-    padding: 2px;
+    margin-left: 5px;
+    padding: 4px;              /* larger tap target */
+    min-width: 24px;
+    min-height: 24px;
     border-radius: 4px;
 }
 
 .tweet-separator-remove:hover {
     background-color: #ddd;
 }
 
+.tweet-separator-remove:active {
+    background-color: #e3e3e3;
+}
+
 .tweet-separator-remove:focus-visible {
     outline: 2px solid #666;
     outline-offset: 2px;
     border-radius: 4px;
 }
 
 .tweet-separator-remove svg {
     width: 16px;
     height: 16px;
 }
+
+/* pair with JS (see plugin.js) instead of inline red */
+.tweet-char-counter.over-limit {
+    color: #b3261e;
+}

Also applies to: 68-76, 78-81

static/ckeditor/ckeditor/plugins/tweet_splitter/plugin.js (5)

129-137: Be robust when detecting separators.

Strict equality on className breaks if extra classes are added. Use classList.contains and nodeType guard.

-        if (node.className === 'tweet-separator') {
+        if (node.nodeType === 1 && node.classList && node.classList.contains('tweet-separator')) {

Apply in both validateTweets() and updateTweetCounters().

Also applies to: 170-178


57-63: Also debounce updates on paste/drop/undo-redo.

Covers more mutation sources beyond key/afterCommandExec.

         editor.on('key', function() {
             scheduleTweetCountersUpdate(editor);;
         });
 
         editor.on('afterCommandExec', function() {
             scheduleTweetCountersUpdate(editor);;
         });
+        editor.on('paste', function() {
+            scheduleTweetCountersUpdate(editor);
+        });
+        editor.on('afterPaste', function() {
+            scheduleTweetCountersUpdate(editor);
+        });
+        editor.on('drop', function() {
+            scheduleTweetCountersUpdate(editor);
+        });

205-207: Avoid inline style for over-limit; use a CSS class.

Enables theming and keeps style in CSS.

-        if (finalLength > TWEET_LIMIT) {
-            counter.style.color = 'red';
-        }
+        counter.classList.toggle('over-limit', finalLength > TWEET_LIMIT);

Pair with the new .tweet-char-counter.over-limit rule in styles.css (see related comment).


21-21: Remove stray double semicolons.

-                scheduleTweetCountersUpdate(editor);;
+                scheduleTweetCountersUpdate(editor);
...
-            scheduleTweetCountersUpdate(editor);;
+            scheduleTweetCountersUpdate(editor);
...
-            scheduleTweetCountersUpdate(editor);;
+            scheduleTweetCountersUpdate(editor);

Also applies to: 58-58, 62-62


155-164: Optional: avoid getData+regex; compute lengths directly from live DOM.

Parsing HTML string is heavier; you already have tweetNodes. Consider deriving textLength from cloned nodes of each tweet and calling twitterLength on their textContent. This reduces serialization cost and keeps DOM/HTML paths consistent.

Also applies to: 184-200

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 732dfe4 and fc31567.

📒 Files selected for processing (2)
  • static/ckeditor/ckeditor/plugins/tweet_splitter/plugin.js (1 hunks)
  • static/ckeditor/ckeditor/plugins/tweet_splitter/styles.css (1 hunks)
🔇 Additional comments (3)
static/ckeditor/ckeditor/plugins/tweet_splitter/styles.css (1)

19-29: Avoid float-based counter layout to prevent overlap with content.

If the first child is a full-width block, the float may overlap. Consider a non-floated pattern (e.g., flex container wrapper or position: sticky). If you keep float, verify across common CKEditor content blocks.

static/ckeditor/ckeditor/plugins/tweet_splitter/plugin.js (2)

23-43: Event binding may misroute updates with multiple editors sharing the same document.

Binding once per nativeDoc and closing over a single editor can desync counters in multi-editor pages.

Prefer per-editor bindings or resolve the editor from the event target:

  • Track a per-editor flag: editor._tweetSplitterRemoveBound.
  • In the click handler, locate the owning editor by traversing to the closest editor.container and map to CKEDITOR.instances.

If you want, I can draft a per-editor-safe handler.


161-161: Regex for splitting separators looks good.

Fix addresses earlier mis-segmentation concern.

Comment thread static/ckeditor/ckeditor/plugins/tweet_splitter/plugin.js
Comment thread static/ckeditor/ckeditor/plugins/tweet_splitter/plugin.js
Comment thread static/ckeditor/ckeditor/plugins/tweet_splitter/plugin.js Outdated
mlv-dev and others added 8 commits August 29, 2025 02:55
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@mlv-dev
Copy link
Copy Markdown
Collaborator Author

mlv-dev commented Aug 29, 2025

done

@mlv-dev mlv-dev marked this pull request as ready for review August 29, 2025 00:09
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.

1 participant