Skip to content

Refactor tokenize_inline into tokenizer; support adjacent links/images - #258

Merged
leynos merged 5 commits into
mainfrom
terragon/fix-tokenize-adjacent-links-rt8j4d
Nov 15, 2025
Merged

leynos merged 5 commits into
mainfrom
terragon/fix-tokenize-adjacent-links-rt8j4d

Conversation

@leynos

@leynos leynos commented Nov 12, 2025 •

Copy link
Copy Markdown
Owner

Summary

  • Refactors the tokenizer into a modular structure and enhances handling of adjacent Markdown links and embedded images, so inputs like word[link](url) tokenize as [word, [link](url)] and word![img](url) tokenize as [word, ![img](url)].
  • Preserves escaped literals such as \\[link](url) and \\![img](url) as single tokens.
  • Adds tests covering these cases, including scenarios with multiple adjacent links and escaped literals.

Changes

Tokenizer

  • Moved tokenize.rs to tokenize/mod.rs and introduced new modules:
    • parsing.rs for link/image parsing
    • scanning.rs for low-level tokenization utilities
  • Updated segment_inline to stop accumulating a token when the next portion starts an embedded link or image. The loop now stops at whitespace, backtick, or '['. It detects a preceding '!' followed by '[' and stops to avoid swallowing an embedded image/link, preserving tokens like [link](url) and ![img](url) as separate tokens.
  • Also adds escaping-aware checks so that escaped brackets do not trigger a split.

Parsing

  • Added public helpers:
    • parse_link_or_image to parse a link or image, preserving nested parentheses
    • looks_like_image_start to detect image starts
    • is_trailing_punctuation for compatibility
    • handle_backtick_fence for code fences

Scanning

  • Added low-level helpers:
    • scan_while
    • collect_range
    • has_odd_backslash_escape_bytes
    • bracket_follows_escaped_bang

Tests

  • In src/wrap/tests.rs, added test cases:
  • Added tests to confirm escaped literals are preserved:
    • case(r"\link")
    • case(r"word\link")
    • case(r"\img")
    • case(r"word\! img")

Test plan

  • Run cargo test to verify new tests pass:
    • cargo test
  • Ensure existing tests continue to pass and that tokenization behavior for non-embedded cases remains unchanged.

📎 Task: https://www.terragonlabs.com/task/01f1c4bc-c82c-460f-9535-0ccc97d04906

Enhance segment_inline by recognizing and splitting tokens before embedded link syntax [link](url) and image syntax ![img](url). This change improves tokenization of inline markdown-like links by treating them as separate tokens. Added corresponding tests in src/wrap/tests.rs to verify correct segmentation of strings containing embedded links and images.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@sourcery-ai

sourcery-ai Bot commented Nov 12, 2025 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

segment_inline now uses a custom loop to detect and split before adjacent markdown links or images, and new unit tests verify correct tokenization of these cases.

Class diagram for updated segment_inline tokenization logic

classDiagram
    class segment_inline {
        +text: &str
        +tokens: Vec<String>
        +scan_while(text, i, predicate)
        +collect_range(text, start, end)
        +Custom loop: stops at whitespace, '`', '['
        +Detects '!' followed by '[' to split before images
    }
Loading

Class diagram for new test cases in tests.rs

classDiagram
    class Tests {
        +case("word[link](url)", &["word", "[link](url)"])
        +case("word[link](url)[another](url2)", &["word", "[link](url)", "[another](url2)"])
        +case("word![img](url)", &["word", "![img](url)"])
    }
Loading

Flow diagram for new tokenization decision in segment_inline

flowchart TD
    A["Start tokenization loop"] --> B["Check current character"]
    B --> C{"Is whitespace, '`', or '['?"}
    C -- Yes --> D["Break and collect token"]
    C -- No --> E{"Is current '!' and next '['?"}
    E -- Yes --> D
    E -- No --> F["Advance to next character"]
    F --> B
Loading

File-Level Changes

Change Details Files
Update segment_inline to split before inline links/images
  • Replaced scan_while with a manual while loop
  • Added break on encountering whitespace, backtick, or '['
  • Detect and break when '!' is followed by '[' to isolate embedded images
src/wrap/tokenize.rs
Add tests for adjacent links and images
  • Introduced rstest cases for "wordlink", multiple adjacent links, and "wordimg"
  • Assert that segment_inline returns separate tokens for text and inline elements
src/wrap/tests.rs

Possibly linked issues

  • #UNKNOWN_ISSUE_ID: PR updates segment_inline to stop at [ or ![ to split adjacent links/images, implementing the issue's proposal.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Nov 12, 2025 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@leynos has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 2 minutes and 52 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 6b7c63a and 911fcf6.

📒 Files selected for processing (2)
  • src/wrap/tokenize/parsing.rs (1 hunks)
  • src/wrap/tokenize/scanning.rs (1 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Summary by CodeRabbit

  • Tests

    • Added test coverage for inline segmentation of embedded links and images, including validation of escaped link literals.
  • Refactor

    • Restructured tokenisation utilities for improved code maintainability and modularity. Public API remains unchanged.

Walkthrough

Refactors the tokenisation logic in src/wrap/tokenize/ by extracting low-level scanning utilities and higher-level parsing helpers into dedicated modules (scanning.rs and parsing.rs). Additionally, new test cases validate inline segmentation for links and escaped literals.

Changes

Cohort / File(s) Summary
Modularised tokeniser structure
src/wrap/tokenize/mod.rs
Introduces mod parsing and mod scanning declarations; imports link/image parsing, scanning utilities, and punctuation detection helpers from the new modules. Updates segment_inline to delegate to modularised functions.
Scanning utilities module
src/wrap/tokenize/scanning.rs
New module exposing low-level byte/character scanning: scan_while, collect_range, has_odd_backslash_escape_bytes, and bracket_follows_escaped_bang. Includes unit tests for boundary handling and multibyte character support.
Parsing helpers module
src/wrap/tokenize/parsing.rs
New module exposing higher-level Markdown parsing: parse_link_or_image, looks_like_image_start, is_trailing_punctuation, and handle_backtick_fence. Includes unit tests for nested parentheses and malformed input handling.
Inline segmentation tests
src/wrap/tests.rs
Adds segment_inline_splits_before_embedded_links and segment_inline_preserves_escaped_link_literals test cases validating correct splitting before links/images and preservation of escaped literals.

Sequence Diagram

sequenceDiagram
    participant seg as segment_inline()
    participant parse as parsing module
    participant scan as scanning module
    
    seg->>scan: scan_while(char predicate)
    scan-->>seg: (idx, slice)
    
    seg->>scan: has_odd_backslash_escape_bytes(pos)
    scan-->>seg: bool (is_escaped)
    
    alt Link/Image detected
        seg->>parse: looks_like_image_start(idx, '!')
        parse-->>seg: bool
        
        seg->>parse: parse_link_or_image(text, idx)
        parse->>scan: has_odd_backslash_escape_bytes(depth tracking)
        scan-->>parse: bool
        parse-->>seg: (link_text, end_idx)
    else Regular text
        seg->>scan: collect_range(start, end)
        scan-->>seg: token_string
    end
    
    seg->>seg: accumulate token
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

  • Focus areas:
    • Verify that parse_link_or_image correctly handles nested parentheses and malformed input fallback logic
    • Confirm bracket_follows_escaped_bang properly detects escaped image starts (\![)
    • Validate that segment_inline correctly delegates to the new modules without altering tokenisation behaviour
    • Check boundary conditions in scan_while for multibyte UTF-8 characters
    • Ensure new tests provide sufficient coverage of the refactored logic paths

Possibly related issues

  • Issue #175: Inline tokenisation changes align with the proposed updates to check for [ and ![ patterns to prevent adjacent links merging. The new looks_like_image_start and parse_link_or_image functions directly implement this boundary detection logic.

Poem

Clean modules rise, refactored and bright,
Scanning and parsing split just right,
Links now escape where they're meant to stay,
Tokenisation improved today! 🎯✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarises the main change: refactoring the tokenizer into a modular structure and adding support for adjacent links/images.
Description check ✅ Passed The description comprehensively outlines all changes made in the PR, including tokenizer refactoring, module structure, segment_inline updates, and new test cases.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

@leynos
leynos marked this pull request as ready for review November 12, 2025 02:39

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes and found some issues that need to be addressed.

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/wrap/tokenize.rs:247` </location>
<code_context>
         } else {
             let start = i;
-            i = scan_while(text, i, |ch| !ch.is_whitespace() && ch != '`');
+            while i < text.len() {
+                let Some(current) = text[i..].chars().next() else {
+                    break;
</code_context>

<issue_to_address>
**issue (review_instructions):** Add behavioural and unit tests to cover the new tokenization logic for '[', '!', and '![' cases.

The new logic in segment_inline introduces handling for '[', '!', and '![' tokens. You must add both behavioural and unit tests to src/wrap/tests.rs to verify this behaviour and ensure correctness.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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

ℹ️ 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".

Comment thread src/wrap/tokenize.rs Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/wrap/tokenize.rs (1)

233-235: Extract the image detection logic to eliminate duplication.

The condition for detecting ![ sequences appears at both lines 233-235 and lines 254-258. Extract this into a helper predicate function to comply with DRY principles.

Apply this refactor:

+fn looks_like_image_start(text: &str, idx: usize, ch: char) -> bool {
+    let after_bang = idx + ch.len_utf8();
+    ch == '!' && after_bang <= text.len() && text[after_bang..].starts_with('[')
+}
+
 pub(super) fn segment_inline(text: &str) -> Vec<String> {
     let mut tokens = Vec::new();
     let bytes = text.as_bytes();
@@ -230,9 +235,7 @@
             continue;
         }
 
-        let after_bang = i + ch.len_utf8();
-        let looks_like_image =
-            ch == '!' && after_bang <= text.len() && text[after_bang..].starts_with('[');
+        let looks_like_image = looks_like_image_start(text, i, ch);
         if ch == '[' || looks_like_image {
             let (tok, mut new_i) = parse_link_or_image(text, i);
             tokens.push(tok);
@@ -252,9 +255,7 @@
                     break;
                 }
                 if current == '!' {
-                    let after_bang = i + current.len_utf8();
-                    if after_bang <= text.len() && text[after_bang..].starts_with('[') {
-                        break;
+                    if looks_like_image_start(text, i, current) {
+                        break;
                     }
                 }

Also applies to: 254-258

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fae4ff8 and 47cc012.

📒 Files selected for processing (2)
  • src/wrap/tests.rs (1 hunks)
  • src/wrap/tokenize.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • src/wrap/tests.rs
  • src/wrap/tokenize.rs
🧬 Code graph analysis (1)
src/wrap/tests.rs (1)
src/wrap/tokenize.rs (1)
  • segment_inline (203-266)
🔍 Remote MCP Deepwiki

Perfect! I now have enough context to provide a comprehensive summary for the PR review. Let me compile the relevant information.

Summary of Additional Context for PR Review

Purpose and Scope

The segment_inline function breaks a single line of Markdown text into a vector of strings, preserving inline code spans, links, images, and surrounding whitespace as separate tokens. This PR addresses a tokenization bug where adjacent links and images merge together instead of being preserved as separate tokens.

Related Issue and Problem Context

Issue #175 addresses how adjacent links and images are tokenized, specifically concerning trailing punctuation and their grouping during text wrapping. The current implementation fails to properly separate adjacent link/image constructs like "word[link](url)[another](url2)" or "word![img](url)".

How the Fix Works

The fix modifies the non-whitespace token-scanning loop in segment_inline to add three stopping conditions:

  1. Stop at whitespace (existing)
  2. Stop at backticks ` (existing)
  3. Stop at [ character (new)
  4. Detect ![ sequences and break before them (new)

The parse_link_or_image function is responsible for parsing Markdown links and images within a line of text, identifying the full extent of a link or image, including nested parentheses in URLs, and returning the parsed string along with the index immediately following it.

Existing Test Coverage Context

The segment_inline function is tested for links with trailing punctuation, images with nested parentheses, multi-backtick inline code fences, unmatched backticks, and escaped backticks. The new tests fill a gap in adjacent link/image handling.

Test Cases Alignment

The three test cases in the PR cover:

  • Simple adjacent link: "word[link](url)" → ["word", "[link](url)"]
  • Multiple adjacent links: "word[link](url)[another](url2)" → ["word", "[link](url)", "[another](url2)"]
  • Adjacent image: "word![img](url)" → ["word", "![img](url)"]

These cases directly address the tokenization bug by validating that the ![ detection prevents images from merging with preceding content.

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (1)
src/wrap/tests.rs (1)

39-50: Excellent test coverage for the adjacent link/image tokenization fix.

The parameterized test cases comprehensively verify the tokenizer's ability to split at [ and ![ boundaries, directly addressing the bug described in the PR objectives. The use of rstest is appropriate and follows the coding guidelines.

Comment thread src/wrap/tokenize.rs Resolved
- Introduced `looks_like_image_start` helper to detect Markdown images
- Updated `segment_inline` to respect escaped link/image literals, avoiding parsing them
- Added tests to ensure escaped links and images are preserved as literals

This prevents misinterpreting escaped link/image syntax, improving tokenizer correctness.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Fix tokenize_inline to split adjacent links and images Fix tokenize_inline to split adjacent links and images; preserve escaped literals Nov 12, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/wrap/tokenize.rs (1)

1-573: File exceeds 400-line limit mandated by coding guidelines.

This file contains 573 lines, significantly exceeding the 400-line maximum. Decompose the module by extracting related functions into separate submodules (e.g., tokenize/scanning.rs for scan_while and byte-level utilities, tokenize/parsing.rs for parse_link_or_image and fence handling).

As per coding guidelines.

♻️ Duplicate comments (1)
src/wrap/tests.rs (1)

52-59: Test failures stem from implementation bug in tokenize.rs.

Cases 3 and 4 fail because the tokeniser incorrectly splits \![img](url) sequences. The backslash escapes the exclamation mark, making \! a literal, but the subsequent [ is then treated as a link start because there's no backslash immediately before it.

The test logic is correct and will pass once the tokenisation bug is addressed. Refer to the review comment on src/wrap/tokenize.rs lines 249-251 for the fix.

Based on pipeline failures.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47cc012 and 6d63c11.

📒 Files selected for processing (2)
  • src/wrap/tests.rs (1 hunks)
  • src/wrap/tokenize.rs (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • src/wrap/tokenize.rs
  • src/wrap/tests.rs
🧬 Code graph analysis (1)
src/wrap/tests.rs (1)
src/wrap/tokenize.rs (1)
  • segment_inline (219-282)
🪛 GitHub Actions: CI
src/wrap/tests.rs

[error] 58-58: Test failed: wrap::tests::segment_inline_preserves_escaped_link_literals::case_3 assertion left != right. Command 'cargo llvm-cov --workspace --summary-only --lcov --output-path lcov.info' exited with code 1.


[error] 58-58: Test failed: wrap::tests::segment_inline_preserves_escaped_link_literals::case_4 assertion left != right. Command 'cargo llvm-cov --workspace --summary-only --lcov --output-path lcov.info' exited with code 1.

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (2)
src/wrap/tokenize.rs (1)

140-154: Clean helper extraction for image-start detection.

The looks_like_image_start function correctly identifies the ![ pattern whilst remaining agnostic about escape sequences. The multibyte-aware offset calculation and clear documentation align with best practices.

src/wrap/tests.rs (1)

39-50: Excellent test coverage for adjacent link splitting.

The segment_inline_splits_before_embedded_links test properly validates that consecutive links and images are split into separate tokens. The parameterised cases cover the key scenarios mentioned in the PR objectives.

Comment thread src/wrap/tokenize/mod.rs Resolved
Add `bracket_follows_escaped_bang` helper to detect when a `[` follows an escaped `!` (i.e. "\!["). Modify parsing logic to treat such brackets as literal text rather than image token start, fixing incorrect tokenization of escaped image syntax.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
…bmodules

The main tokenize.rs file was split into mod.rs (public API) and two new
modules: parsing.rs (higher-level inline Markdown parsing helpers) and
scanning.rs (byte-level scanning utilities). This separation improves
code organization by isolating parsing logic and low-level scanning
routines from the public tokenizer interface.

Additionally, several functions previously defined directly in
tokenize.rs were moved into these new submodules, reducing file size and
making the codebase more modular and maintainable.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Fix tokenize_inline to split adjacent links and images; preserve escaped literals Refactor tokenize_inline into tokenizer; support adjacent links/images Nov 14, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 536d2c3 and 6b7c63a.

📒 Files selected for processing (3)
  • src/wrap/tokenize/mod.rs (3 hunks)
  • src/wrap/tokenize/parsing.rs (1 hunks)
  • src/wrap/tokenize/scanning.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • src/wrap/tokenize/scanning.rs
  • src/wrap/tokenize/parsing.rs
  • src/wrap/tokenize/mod.rs
🧬 Code graph analysis (2)
src/wrap/tokenize/parsing.rs (1)
src/wrap/tokenize/scanning.rs (3)
  • collect_range (42-44)
  • has_odd_backslash_escape_bytes (52-63)
  • scan_while (20-32)
src/wrap/tokenize/mod.rs (2)
src/wrap/tokenize/parsing.rs (4)
  • handle_backtick_fence (103-123)
  • is_trailing_punctuation (96-101)
  • looks_like_image_start (76-82)
  • parse_link_or_image (23-66)
src/wrap/tokenize/scanning.rs (4)
  • bracket_follows_escaped_bang (69-74)
  • collect_range (42-44)
  • has_odd_backslash_escape_bytes (52-63)
  • scan_while (20-32)
🔍 Remote MCP Deepwiki

Relevant facts discovered (concise, review-focused)

  • New tokenizer split: segment_inline logic was moved into two new modules tokenize/parsing.rs and tokenize/scanning.rs and segment_inline now delegates link/image parsing to parse_link_or_image and scanning/escaping to helpers like has_odd_backslash_escape_bytes, bracket_follows_escaped_bang, collect_range, scan_while. (See repo docs on tokenization and wrap module organization.)

  • Tests added exercise exactly the intended behavior:

    • Cases expecting a split before an embedded link/image: e.g. "wordlink" → ["word", "link"]; multi-adjacent links/images also asserted.
    • Cases ensuring escaped bracket/bang are preserved as single token: e.g. r"[link](url)" and r"!img". These tests are placed alongside other wrap tests and target segment_inline/inline segmentation behavior.
  • Tokenization and wrapping are central to the pipeline:

    • The wrapping pipeline uses tokenization (tokenize_markdown, Token enum) and a FenceTracker to avoid modifying fenced code; process_stream_inner runs fence normalization / HTML conversion → core loop (table buffering + reflow) → postprocessing (headings/code-emphasis/wrap/ellipsis/footnotes). Any changes to inline segmentation can affect determine_token_span, wrap_text, and downstream postprocessing.
  • Key integration points to check when reviewing the PR:

    • Fence handling: ensure new parsing/scanning helpers respect FenceTracker semantics so code fences remain unaffected. The pipeline flushes buffers on fence lines (handle_fence_line) and token-based transforms skip code tokens.
    • determine_token_span / grouping rules: the wrapping logic groups related tokens (links, code spans, trailing punctuation); the new parse_link_or_image must preserve grouping widths and punctuation attachment behavior used by LineBuffer/width calculations.
    • Unicode/multibyte: wrapping and table reflow use unicode-width for visual widths—segmentation must not split multibyte characters or alter width calculations.
    • Escaping semantics: helpers like has_odd_backslash_escape_bytes and bracket_follows_escaped_bang must correctly detect escaped \[ and \! so escaped literals are preserved as tests expect.
  • Test & CI expectations:

    • Run cargo test (unit, integration, CLI tests) and ensure new and existing tests pass; CI enforces fmt/clippy/tests. The repo uses strict CI (format, markdown-lint, clippy as -D warnings, full tests + coverage).

Concrete review checklist (apply when reviewing code/behavior):

  • Verify parse_link_or_image handles nested parentheses and returns correct slice + index for valid and malformed inputs (no off-by-one; tests for nested parentheses exist in parsing module).
  • Confirm looks_like_image_start distinguishes escaped ! (i.e., not preceded by odd backslash) and that bracket_follows_escaped_bang correctly detects \![.
  • Ensure has_odd_backslash_escape_bytes accounts for raw bytes and multibyte contexts (used by scanning).
  • Check that segment_inline stops token accumulation at [ or ! only when those are not escaped and that it preserves previous token punctuation attachment rules.
  • Run full test suite including wrap/tokenize tests (they exist in repo) and run any added new tests in src/wrap/tests.rs.
  • Manual edge-case suggestions to run or add tests for reviewers: adjacent links/images mixed with multibyte text, multiple escaped backslashes before [/! (odd vs even), code-fence boundaries immediately adjacent to links/images, and interactions with determine_token_span grouping + trailing punctuation.

Sources used for this summary: project documentation/wiki and module overviews extracted from repository docs.

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: build-test
🔇 Additional comments (9)
src/wrap/tokenize/scanning.rs (3)

20-32: LGTM!

The UTF-8 handling is correct, and the function properly advances by character byte length.


42-43: LGTM!

Returning String is appropriate here since callers accumulate tokens into Vec<String>.


52-62: LGTM!

The escape detection logic correctly counts preceding backslashes and returns true for an odd count.

src/wrap/tokenize/parsing.rs (3)

76-82: LGTM!

The image start detection correctly checks for '!' followed by '['.


96-100: LGTM!

Including '!' as trailing punctuation is correct here—it applies to punctuation after a link's closing parenthesis, not the '!' at an image start.


103-123: LGTM!

The fence matching logic correctly handles fence length and escape sequences.

src/wrap/tokenize/mod.rs (3)

6-14: LGTM!

The module structure cleanly separates scanning and parsing concerns, and the imports correctly expose the needed helpers to this module.


85-87: LGTM!

The escape detection correctly prevents parsing of escaped brackets and image markers.


98-115: Test coverage does not validate all four edge cases mentioned in the review.

The implementation of bracket_follows_escaped_bang and has_odd_backslash_escape_bytes correctly handles escape detection. However, the test suite at src/wrap/tests.rs covers only two of the four edge cases:

  • \![img](url) → single token (line 55) ✓
  • \\![img](url) → splits correctly ✗ Missing
  • \[link](url) → single token (line 53) ✓
  • Multiple consecutive escaped sequences ✗ Missing

Add parametrised test cases for \\![img](url) and a multi-sequence case (e.g. \!\[two](url) or \[a]\[b]) to segment_inline_preserves_escaped_link_literals in src/wrap/tests.rs.

Comment thread src/wrap/tokenize/parsing.rs Resolved
Comment thread src/wrap/tokenize/parsing.rs Resolved
Comment thread src/wrap/tokenize/scanning.rs Resolved
Comment thread src/wrap/tokenize/scanning.rs Resolved
…s support

Refactored the parse_link_or_image function into smaller helpers (parse_link_text, parse_link_url, fallback_single_char) to improve clarity and maintainability. Updated parsing logic to correctly handle nested parentheses in link URLs and added comprehensive tests for these cases.

Also improved scanning.rs with additional test coverage and clarified bracket_follows_escaped_bang behavior.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos
leynos merged commit d427f7b into main Nov 15, 2025
3 checks passed
@leynos
leynos deleted the terragon/fix-tokenize-adjacent-links-rt8j4d branch November 15, 2025 01:21
leynos added a commit that referenced this pull request Sep 29, 2026
#570 (#561) lets the wrapper break only at source whitespace, so an
opener written directly before its label can no longer be split and
`[a]` is a fixed point, whatever the label's characters. This records
the fix. The corpus gains the issue's two reproductions as cases E2
and E3, and `tests/issue_507_bracket_label_wrap_is_idempotent.rs` runs
them through the binary. A property grows prose to the wrap boundary in
front of labels of letters and digits. ADR 0006 gains a 2026-09-27
addendum retiring the digit-only residue, and ADR 0010 points to it.

The alphabetic shape dates from #258 (d427f7b), which split `[` from
its label. The #505 tests generated only digit labels.
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