Class: Docsmith::Diff::Renderers::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/docsmith/diff/renderers/base.rb

Overview

Line-level diff renderer, and the base for the format-aware parsers.

Subclasses override #tokenize only. All grouping, offset arithmetic, and HTML rendering lives here, so every content type produces the same edit structure and only the token boundaries differ.

An edit is a contiguous run of changes collapsed into one entry:

{ type: :replace,
old:  { start: 0, end: 11, line: 1, column: 1, text: "my document" },
new:  { start: 0, end: 30, line: 1, column: 1, text: "<h1>..." } }

Both sides are always present. A pure insertion carries a zero-width old span at the insertion point; a pure deletion a zero-width new span. Every edit therefore has identical keys, and offsets on the two sides never share a coordinate space.

text is sliced straight out of the source between the run's offsets, so whitespace the tokenizer discarded is preserved verbatim and both documents round-trip exactly.

Direct Known Subclasses

Parsers::Html, Parsers::Markdown

Constant Summary collapse

OLD_SIDE =

Actions that consume a token from the old side / the new side.

%w[- !].freeze
NEW_SIDE =
%w[+ !].freeze

Instance Method Summary collapse

Instance Method Details

#compute(old_content, new_content) ⇒ Array<Hash>

Computes grouped edits between two content strings.

Parameters:

  • old_content (String)
  • new_content (String)

Returns:

  • (Array<Hash>)

    edit hashes with :type, :old and :new



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/docsmith/diff/renderers/base.rb', line 58

def compute(old_content, new_content)
  old_content = old_content.to_s
  new_content = new_content.to_s
  old_tokens  = tokenize(old_content)
  new_tokens  = tokenize(new_content)

  edits = []
  run   = []

  ::Diff::LCS.sdiff(old_tokens.map(&:first), new_tokens.map(&:first)).each do |hunk|
    if hunk.action == "="
      edits << build_edit(run, old_tokens, new_tokens, old_content, new_content) unless run.empty?
      run = []
    else
      run << hunk
    end
  end
  edits << build_edit(run, old_tokens, new_tokens, old_content, new_content) unless run.empty?

  edits
end

#render_html(edits) ⇒ String

Renders grouped edits as an HTML diff representation.

Parameters:

  • edits (Array<Hash>)

Returns:

  • (String)

    HTML string



84
85
86
87
88
89
90
91
92
93
# File 'lib/docsmith/diff/renderers/base.rb', line 84

def render_html(edits)
  lines = edits.map do |edit|
    case edit[:type]
    when :insert  then insertion(edit[:new][:text])
    when :delete  then deletion(edit[:old][:text])
    when :replace then deletion(edit[:old][:text]) + insertion(edit[:new][:text])
    end
  end
  %(<div class="docsmith-diff">#{lines.join("\n")}</div>)
end

#tokenize(content) ⇒ Array<Array(String, Integer)>

Splits content into [text, start_offset] pairs. Lines here, excluding the newline itself. Override in subclasses for other content types.

The newline is deliberately not part of the token: including it made appending a line report the previous line as changed, because that line gained a trailing newline. Empty lines are preserved as empty tokens so that blank-line changes are still detected.

Parameters:

  • content (String)

Returns:

  • (Array<Array(String, Integer)>)


44
45
46
47
48
49
50
51
# File 'lib/docsmith/diff/renderers/base.rb', line 44

def tokenize(content)
  offset = 0
  content.to_s.split("\n", -1).map do |line|
    token = [line, offset]
    offset += line.length + 1
    token
  end
end