Module: Docsmith::Comments::Anchor

Defined in:
lib/docsmith/comments/anchor.rb

Overview

Builds and migrates range anchors for inline comments. An anchor captures character offsets and a content hash of the selected text so the comment can be relocated when content changes between versions.

Constant Summary collapse

ACTIVE =
"active"
DRIFTED =
"drifted"
ORPHANED =
"orphaned"

Class Method Summary collapse

Class Method Details

.build(content, start_offset:, end_offset:) ⇒ Hash

Builds anchor_data for a new range comment.

Parameters:

  • content (String)

    the version content at comment time

  • start_offset (Integer)

    character offset of selection start (inclusive)

  • end_offset (Integer)

    character offset of selection end (exclusive)

Returns:

  • (Hash)

    anchor_data hash ready to store on the Comment



21
22
23
24
25
26
27
28
29
30
# File 'lib/docsmith/comments/anchor.rb', line 21

def self.build(content, start_offset:, end_offset:)
  anchored_text = content[start_offset...end_offset].to_s
  {
    start_offset:  start_offset,
    end_offset:    end_offset,
    content_hash:  Digest::SHA256.hexdigest(anchored_text),
    anchored_text: anchored_text,
    status:        ACTIVE
  }
end

.migrate(content, anchor_data) ⇒ Hash

Attempts to migrate an existing anchor to new version content.

Strategy:

  1. Try exact offset — if SHA256 of text at same offsets matches, return ACTIVE.
  2. Search the full content for the original anchored text — return DRIFTED with new offsets.
  3. If not found anywhere, return ORPHANED.

Parameters:

  • content (String)

    new version content

  • anchor_data (Hash)

    existing anchor_data (string keys from JSON storage)

Returns:

  • (Hash)

    updated anchor_data with new :status



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/docsmith/comments/anchor.rb', line 42

def self.migrate(content, anchor_data)
  start_off     = anchor_data["start_offset"]
  end_off       = anchor_data["end_offset"]
  original_hash = anchor_data["content_hash"]
  original_text = anchor_data["anchored_text"]

  # An anchor missing its offsets or its captured text cannot be relocated.
  # Previously this reached content[nil...nil] / content.index(nil) and
  # raised TypeError out of a migration loop.
  return anchor_data.merge("status" => ORPHANED) if original_text.nil? || original_text.empty?

  # 1. Exact offset check
  candidate = start_off.nil? || end_off.nil? ? "" : content[start_off...end_off].to_s
  return anchor_data.merge("status" => ACTIVE) if Digest::SHA256.hexdigest(candidate) == original_hash

  # 2. Full-text search for relocated text
  idx = content.index(original_text)
  if idx
    new_end = idx + original_text.length
    return anchor_data.merge(
      "start_offset" => idx,
      "end_offset"   => new_end,
      "status"       => DRIFTED
    )
  end

  # 3. Orphaned — text no longer exists
  anchor_data.merge("status" => ORPHANED)
end