Class: Crawdad::Paragraph

Inherits:
Object
  • Object
show all
Includes:
Tokens
Defined in:
lib/crawdad/paragraph.rb,
lib/crawdad/ffi/paragraph.rb

Defined Under Namespace

Modules: C

Constant Summary

Constants included from Tokens

Tokens::Type

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Tokens

#box, #box_content, #glue, #glue_shrink, #glue_stretch, #penalty, #penalty_flagged?, #penalty_penalty, #token_type, #token_width

Constructor Details

#initialize(stream, options = {}) ⇒ Paragraph

Returns a new instance of Paragraph.



14
15
16
17
18
19
# File 'lib/crawdad/paragraph.rb', line 14

def initialize(stream, options={})
  @stream = stream
  @width = options[:width]
  @flagged_penalty = options[:flagged_penalty] || 3000
  @fitness_penalty = options[:fitness_penalty] || 100
end

Instance Attribute Details

#widthObject

Width of the paragraph of text.



23
24
25
# File 'lib/crawdad/paragraph.rb', line 23

def width
  @width
end

Instance Method Details

#adjustment_ratio(node_a, b) ⇒ Object

Calculates the adjustment ratio r by which a line from a to b would have to be adjusted to fit in the given length. r==0 means the natural widths are perfect. r==-1 means all of the shrinkability has been used; r==1 means all of the stretchability has been used.

Arguments:

node_a

Breakpoint node of our starting point (on the active list).

b

Index (into stream) of the breakpoint under consideration.



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/crawdad/paragraph.rb', line 173

def adjustment_ratio(node_a, b)
  item_b = @stream[b]
  # Find the width from a to b.
  w = @total_width - node_a.total_width
  # Add penalty width (hyphen) if we are breaking at a penalty
  w += token_width(item_b) if token_type(item_b) == :penalty
  target_width = @width

  case
  when w < target_width
    stretch = @total_stretch - node_a.total_stretch
    (stretch > 0) ? (target_width - w) / stretch.to_f : Infinity
  when w > target_width
    shrink = @total_shrink - node_a.total_shrink
    (shrink > 0) ? (target_width - w) / shrink.to_f : Infinity
  else 0
  end
end

For each item before which we could break, yields two values:

item

The item we can break before (glue or penalty).

i

The index of item in the stream.

Updates the @total_width, @total_stretch, and @total_shrink variables as it moves over the stream, to allow quick calculation of the width/stretch/shrink from the last breakpoint node.

Legal breakpoints are either:

  • glue immediately following a box, or

  • a penalty less than positive infinity.



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/crawdad/paragraph.rb', line 138

def each_legal_breakpoint
  @total_width   = 0
  @total_stretch = 0
  @total_shrink  = 0

  @stream.each_with_index do |item, i|
    case token_type(item)
    when :box
      @total_width += token_width(item)
    when :glue
      # We can break here if we immediately follow a box.
      yield(item, i) if token_type(@stream[i-1]) == :box
      @total_width   += token_width(item)
      @total_stretch += glue_stretch(item)
      @total_shrink  += glue_shrink(item)
    when :penalty
      # We can break here unless inhibited by an infinite penalty.
      yield(item, i) unless penalty_penalty(item) == Infinity
    else
      raise ArgumentError, "Unknown item: #{item.inspect}"
    end
  end
end

#lines(threshold = 5) ⇒ Object

Returns an array of optimally sized lines. Each line in the array consists of two elements [tokens, breakpoint]. tokens is an array of tokens taken sequentially from the input stream. breakpoint is a Crawdad::Breakpoint object representing data about the line (primarily the adjustment ratio).



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/crawdad/paragraph.rb', line 30

def lines(threshold=5)
  ls = []
  breakpoints = optimum_breakpoints(threshold)

  # When we break on penalties, we want them to show up at the *end* of the
  # line so that we can put hyphens there if needed. So adjust the
  # breakpoint positions to make that the case.
  breakpoints.each do |b|
    b.position += 1 if token_type(@stream[b.position]) == :penalty
  end

  breakpoints.each_cons(2) do |a, b|
    last = (b == breakpoints[-1]) ? b.position : b.position - 1
    ls << [@stream[a.position..last], b]
  end
  ls
end

#optimum_breakpoints(threshold = 5) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/crawdad/paragraph.rb', line 48

def optimum_breakpoints(threshold=5)
  active_nodes = [Breakpoint.starting_node]
  each_legal_breakpoint do |item, bi|
    # "Main Loop" (Digital Typography p. 118)

    if active_nodes.empty?
      raise "No feasible solution. Try relaxing threshold."
    end

    ai = 0

    while active_nodes[ai]
      # For each fitness class, keep track of the nodes with the fewest
      # demerits so far.
      best = [nil] * 4

      while a = active_nodes[ai]
        j = a.line + 1 # current line
        r = adjustment_ratio(a, bi)

        if r < -1 || (token_type(item) == :penalty && 
                      penalty_penalty(item) == -Infinity && 
                      a.position < @stream.length - 1)
          active_nodes.delete_at(ai)
        else
          ai += 1
        end

        if r >= -1 && r <= threshold
          d = calculate_demerits(r, item, a) + a.total_demerits
          c = self.class.fitness_class(r)

          # Penalize consecutive lines more than one fitness class away from
          # each other.
          if (c - a.fitness_class).abs > 1
            d += @fitness_penalty
          end

          # Update high scores if this is a new best.
          if best[c].nil? || d < best[c][:demerits]
            best[c] = {:node => a, :demerits => d, :ratio => r}
          end
        end

        # Add nodes to the active list before moving to the next line.
        if (next_node = active_nodes[ai]) && next_node.line >= j
          break
        end
      end

      # If we found any best nodes, add them to the active list.
      if ai && ai < active_nodes.length - 1
        active_nodes[ai, 0] = new_active_nodes(best, bi)
      else
        active_nodes.concat new_active_nodes(best, bi)
      end
    end

  end

  # At this point, everything in active_nodes should point to the final
  # element of our stream (the forced break). Now we pick the one with the
  # fewest total demerits.
  
  node = active_nodes.sort_by { |n| n.total_demerits }.first

  nodes = []
  begin
    nodes.unshift(node)
  end while node = node.previous

  nodes
end