epub-cfi 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,88 @@
1
+ require "epub/cfi/parser.tab"
2
+
3
+ class EPUB::CFI::Parser
4
+ include Comparable
5
+
6
+ UNICODE_CHARACTER_EXCLUDING_SPECIAL_CHARS_AND_SPACE_AND_DOT_AND_COLON_AND_TILDE_AND_ATMARK_AND_SOLIDUS_AND_EXCLAMATION_MARK_PATTERN = /\u0009|\u000A|\u000D|[\u0022-\u0027]|[\u002A-\u002B]|\u002D|[\u0030-\u0039]|\u003C|[\u003E-\u0040]|[\u0041-\u005A]|\u005C|[\u005F-\u007D]|[\u007F-\uD7FF]|[\uE000-\uFFFD]|[\u10000-\u10FFFF]/ # excluding special chars and space(\u0020) and dot(\u002E) and colon(\u003A) and tilde(\u007E) and atmark(\u0040) and solidus(\u002F) and exclamation mark(\u0021)
7
+ UNICODE_CHARACTER_PATTERN = Regexp.union(UNICODE_CHARACTER_EXCLUDING_SPECIAL_CHARS_AND_SPACE_AND_DOT_AND_COLON_AND_TILDE_AND_ATMARK_AND_SOLIDUS_AND_EXCLAMATION_MARK_PATTERN, Regexp.new(Regexp.escape(EPUB::CFI::SPECIAL_CHARS), / \.:~@!/))
8
+
9
+ class << self
10
+ def parse(string, debug: false)
11
+ new(debug: debug).parse(string)
12
+ end
13
+ end
14
+
15
+ def initialize(debug: false)
16
+ @yydebug = debug
17
+ super()
18
+ end
19
+
20
+ def parse(string)
21
+ if string.start_with? 'epubcfi('
22
+ string = string['epubcfi('.length .. -2]
23
+ end
24
+ @scanner = StringScanner.new(string, true)
25
+ @q = []
26
+ until @scanner.eos?
27
+ case
28
+ when @scanner.scan(/[1-9]/)
29
+ @q << [:DIGIT_NON_ZERO, @scanner[0]]
30
+ when @scanner.scan(/0/)
31
+ @q << [:ZERO, @scanner[0]]
32
+ when @scanner.scan(/ /)
33
+ @q << [:SPACE, @scanner[0]]
34
+ when @scanner.scan(/\^/)
35
+ @q << [:CIRCUMFLEX, @scanner[0]]
36
+ when @scanner.scan(/\[/)
37
+ @q << [:OPENING_SQUARE_BRACKET, @scanner[0]]
38
+ when @scanner.scan(/\]/)
39
+ @q << [:CLOSING_SQUARE_BRACKET, @scanner[0]]
40
+ when @scanner.scan(/\(/)
41
+ @q << [:OPENING_PARENTHESIS, @scanner[0]]
42
+ when @scanner.scan(/\)/)
43
+ @q << [:CLOSING_PARENTHESIS, @scanner[0]]
44
+ when @scanner.scan(/,/)
45
+ @q << [:COMMA, @scanner[0]]
46
+ when @scanner.scan(/;/)
47
+ @q << [:SEMICOLON, @scanner[0]]
48
+ when @scanner.scan(/=/)
49
+ @q << [:EQUAL, @scanner[0]]
50
+ when @scanner.scan(/\./)
51
+ @q << [:DOT, @scanner[0]]
52
+ when @scanner.scan(/:/)
53
+ @q << [:COLON, @scanner[0]]
54
+ when @scanner.scan(/~/)
55
+ @q << [:TILDE, @scanner[0]]
56
+ when @scanner.scan(/@/)
57
+ @q << [:ATMARK, @scanner[0]]
58
+ when @scanner.scan(/\//)
59
+ @q << [:SOLIDUS, @scanner[0]]
60
+ when @scanner.scan(/!/)
61
+ @q << [:EXCLAMATION_MARK, @scanner[0]]
62
+ when @scanner.scan(UNICODE_CHARACTER_EXCLUDING_SPECIAL_CHARS_AND_SPACE_AND_DOT_AND_COLON_AND_TILDE_AND_ATMARK_AND_SOLIDUS_AND_EXCLAMATION_MARK_PATTERN)
63
+ @q << [:UNICODE_CHARACTER_EXCLUDING_SPECIAL_CHARS_AND_SPACE_AND_DOT_AND_COLON_AND_TILDE_AND_ATMARK_AND_SOLIDUS_AND_EXCLAMATION_MARK, @scanner[0]]
64
+ else
65
+ raise 'unexpected character'
66
+ end
67
+ end
68
+ @q << [false, false]
69
+
70
+ do_parse
71
+ end
72
+
73
+ def next_token
74
+ @q.shift
75
+ end
76
+ end
77
+
78
+ module EPUB::CFI
79
+ module_function
80
+
81
+ def parse(string)
82
+ EPUB::CFI::Parser.parse(string)
83
+ end
84
+ end
85
+
86
+ def EPUB::CFI(string)
87
+ EPUB::CFI.parse(string)
88
+ end
@@ -0,0 +1,184 @@
1
+ class EPUB::CFI::Parser
2
+ rule
3
+
4
+ fragment : path range_zero_or_one
5
+ {
6
+ if val[1]
7
+ result = CFI::Range.from_parent_and_start_and_end(val[0], *val[1])
8
+ else
9
+ result = CFI::Location.new(val[0])
10
+ end
11
+ }
12
+
13
+ range_zero_or_one : range
14
+ |
15
+
16
+ path : step local_path
17
+ {
18
+ path, redirected_path = *val[1]
19
+ path.steps.unshift val[0]
20
+ result = val[1]
21
+ }
22
+
23
+ range : COMMA local_path COMMA local_path
24
+ {result = [val[1], val[3]]}
25
+
26
+ local_path : step_zero_or_more redirected_path
27
+ {result = [CFI::Path.new(val[0])] + val[1]}
28
+ | step_zero_or_more offset_zero_or_one
29
+ {result = [CFI::Path.new(val[0], val[1])]}
30
+
31
+ step_zero_or_more : step_zero_or_more step
32
+ {result = val[0] + [val[1]]}
33
+ | step
34
+ {result = [val[0]]}
35
+ |
36
+ {result = []}
37
+
38
+ redirected_path : EXCLAMATION_MARK offset
39
+ {result = [CFI::Path.new([], val[1])]}
40
+ | EXCLAMATION_MARK path
41
+ {result = val[1]}
42
+
43
+ step : SOLIDUS integer assertion_part_zero_or_one
44
+ {
45
+ assertion = val[2] ? CFI::IDAssertion.new(val[2][0], val[2][2]) : nil
46
+ result = CFI::Step.new(val[1].to_i, assertion)
47
+ }
48
+
49
+ offset_zero_or_one : offset
50
+ |
51
+
52
+ offset : COLON integer assertion_part_zero_or_one
53
+ {
54
+ assertion = val[2] ? CFI::TextLocationAssertion.new(*val[2]) : nil
55
+ result = CFI::CharacterOffset.new(val[1].to_i, assertion)
56
+ }
57
+ | spatial_offset assertion_part_zero_or_one
58
+ {result = CFI::TemporalSpatialOffset.new(nil, val[0][0].to_f, val[0][1].to_f, val[2])}
59
+ | TILDE number spatial_offset_zero_or_one assertion_part_zero_or_one
60
+ {
61
+ x = val[2] ? val[2][0].to_f : nil
62
+ y = val[2] ? val[2][1].to_f : nil
63
+ result = CFI::TemporalSpatialOffset.new(val[1].to_f, x, y, val[3])
64
+ }
65
+
66
+ spatial_offset_zero_or_one : spatial_offset
67
+ |
68
+
69
+ spatial_offset : ATMARK number COLON number
70
+ {result = [val[1], val[3]]}
71
+
72
+ assertion_part_zero_or_one : opening_square_bracket assertion closing_square_bracket
73
+ {result = val[1]}
74
+ |
75
+
76
+ number : DIGIT_NON_ZERO digit_zero_or_more fractional_portion_zero_or_one
77
+ {result = val.join}
78
+ | ZERO fractional_portion_zero_or_one
79
+ {result = val.join}
80
+
81
+ fractional_portion_zero_or_one : fractional_portion
82
+ |
83
+
84
+ fractional_portion : DOT digit_zero_or_more DIGIT_NON_ZERO
85
+ {result = val.join}
86
+ | DOT DIGIT_NON_ZERO
87
+ {result = val.join}
88
+
89
+ integer : ZERO
90
+ | DIGIT_NON_ZERO digit_zero_or_more
91
+ {result = val.join}
92
+
93
+ digit_zero_or_more : digit_zero_or_more digit
94
+ {result = val.join}
95
+ | digit
96
+ |
97
+
98
+ assertion : value_csv_one_or_two parameter_zero_or_more
99
+ {result = [val[0][0], val[0][1], val[1]]} # Cannot see id assertion or text location assertion when val[0]'s length is 1. It can be done by context.
100
+ | COMMA value parameter_zero_or_more
101
+ {result = [nil, val[1], val[2]]}
102
+ | parameter parameter_zero_or_more
103
+ {result = [nil, nil, val[0].merge(val[1])]} # Cannot see id assertion or text location assertion when val[0]'s length is 1. It can be done by context. In EPUBCFI 3.0.1 spec, only side-bias parameter is defined and we can say it's text location assertion of the assertion has parameters. But when the spec is extended and other parameter definitions added, we might become not able to say so.
104
+
105
+ value_csv_one_or_two : value COMMA value
106
+ {result = [val[0], val[2]]}
107
+ | value
108
+ {result = [val[0]]}
109
+
110
+ parameter_zero_or_more : parameter_zero_or_more parameter
111
+ {result = val[0].merge(val[1])}
112
+ | parameter
113
+ {result = val[0]}
114
+ |
115
+ {result = {}}
116
+
117
+ parameter : SEMICOLON value_no_space EQUAL csv
118
+ {result = {val[1] => val[3]}}
119
+
120
+ csv : csv COMMA value
121
+ {result = val[0] + [val[2]]}
122
+ | value
123
+ {result = [val[0]]}
124
+
125
+ value : string_escaped_special_chars
126
+ {result = val[0]}
127
+
128
+ value_no_space: string_escaped_special_chars_excluding_space
129
+
130
+ escaped_special_chars : CIRCUMFLEX CIRCUMFLEX
131
+ {result = val[1]}
132
+ | CIRCUMFLEX square_brackets
133
+ {result = val[1]}
134
+ | CIRCUMFLEX parentheses
135
+ {result = val[1]}
136
+ | CIRCUMFLEX COMMA
137
+ {result = val[1]}
138
+ | CIRCUMFLEX SEMICOLON
139
+ {result = val[1]}
140
+ | CIRCUMFLEX EQUAL
141
+ {result = val[1]}
142
+
143
+ character_escaped_special : character_excluding_special_chars
144
+ | escaped_special_chars
145
+
146
+ string_escaped_special_chars : string_escaped_special_chars character_escaped_special
147
+ {result = val.join}
148
+ | character_escaped_special
149
+ {result = val[0]}
150
+
151
+ string_escaped_special_chars_excluding_space : string_escaped_special_chars_excluding_space character_escaped_special_excluding_space
152
+ | character_escaped_special_excluding_space
153
+
154
+ character_escaped_special_excluding_space : character_excluding_special_chars_and_space
155
+ | escaped_special_chars
156
+
157
+ digit : ZERO
158
+ | DIGIT_NON_ZERO
159
+
160
+ square_brackets : opening_square_bracket
161
+ | closing_square_bracket
162
+
163
+ opening_square_bracket : OPENING_SQUARE_BRACKET
164
+
165
+ closing_square_bracket : CLOSING_SQUARE_BRACKET
166
+
167
+ parentheses : OPENING_PARENTHESIS
168
+ | CLOSING_PARENTHESIS
169
+
170
+ character_excluding_special_chars : character_excluding_special_chars_and_space
171
+ | SPACE
172
+
173
+ character_excluding_special_chars_and_space : character_excluding_special_chars_and_space_and_dot_and_colon_and_tilde_and_atmark_and_solidus_and_exclamation_mark
174
+ | DOT
175
+ | COLON
176
+ | TILDE
177
+ | ATMARK
178
+ | SOLIDUS
179
+ | EXCLAMATION_MARK
180
+
181
+ character_excluding_special_chars_and_space_and_dot_and_colon_and_tilde_and_atmark_and_solidus_and_exclamation_mark : UNICODE_CHARACTER_EXCLUDING_SPECIAL_CHARS_AND_SPACE_AND_DOT_AND_COLON_AND_TILDE_AND_ATMARK_AND_SOLIDUS_AND_EXCLAMATION_MARK
182
+ | digit
183
+
184
+ end
@@ -0,0 +1,23 @@
1
+ #
2
+ # Copyright (c) 2017 KITAITI Makoto (KitaitiMakoto at gmail.com)
3
+ #
4
+ # EPUB CFI is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Lesser General Public License as published
6
+ # by the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # EPUB CFI is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public License
15
+ # along with EPUB CFI. If not, see <http://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ module EPUB
19
+ module CFI
20
+ # epub-cfi version
21
+ VERSION = "0.1.0"
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ require "simplecov"
2
+ SimpleCov.start do
3
+ add_filter "/test/"
4
+ end
5
+
6
+ require 'test/unit'
7
+ require "test/unit/notify"
8
+
9
+ require "pretty_backtrace"
10
+ PrettyBacktrace.enable
11
+
12
+ class Test::Unit::TestCase
13
+ end
@@ -0,0 +1,196 @@
1
+ # coding: utf-8
2
+ require 'helper'
3
+ require 'epub/cfi'
4
+
5
+ class TestCFI < Test::Unit::TestCase
6
+ def test_escape
7
+ assert_equal '^^^[^]^(^)^,^;^=', EPUB::CFI.escape('^[](),;=')
8
+ end
9
+
10
+ def test_unescape
11
+ assert_equal '^[](),;=', EPUB::CFI.unescape('^^^[^]^(^)^,^;^=')
12
+ end
13
+
14
+ class TestPath < self
15
+ data([
16
+ 'epubcfi(/6/14[chap05ref]!/4[body01]/10/2/1:3[2^[1^]])',
17
+ 'epubcfi(/6/4!/4/10/2/1:3[Ф-"spa ce"-99%-aa^[bb^]^^])',
18
+ 'epubcfi(/6/4!/4/10/2/1:3[Ф-"spa%20ce"-99%25-aa^[bb^]^^])',
19
+ 'epubcfi(/6/4!/4/10/2/1:3[%d0%a4-"spa%20ce"-99%25-aa^[bb^]^^])',
20
+ 'epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3[yyy])',
21
+ 'epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/1:3[xx,y])',
22
+ 'epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3[,y])',
23
+ 'epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3[;s=b])',
24
+ 'epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3[yyy;s=b])',
25
+ 'epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2[;s=b])',
26
+ 'epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/3:10)',
27
+ 'epubcfi(/6/4[chap01ref]!/4[body01]/16[svgimg])',
28
+ 'epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/1:0)',
29
+ 'epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:0)',
30
+ 'epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3)',
31
+ ].reduce({}) {|data, cfi|
32
+ data[cfi] = cfi
33
+ data
34
+ })
35
+ def test_to_s(cfi)
36
+ assert_equal cfi, EPUB::CFI::Parser.parse(cfi).to_s
37
+ end
38
+
39
+ def test_compare
40
+ assert_equal -1, epubcfi('/6/4[id]') <=> epubcfi('/6/5')
41
+ assert_equal 0, epubcfi('/6/4') <=> epubcfi('/6/4')
42
+ assert_equal 1, epubcfi('/6/4') <=> epubcfi('/4/6')
43
+ assert_equal 1, epubcfi('/6/4!/4@3:7') <=> epubcfi('/6/4!/4')
44
+ assert_equal 1,
45
+ epubcfi('/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3[yyy]') <=>
46
+ epubcfi('/6/4[chap01ref]!/4[body01]/10[para05]/1:3[xx,y]')
47
+ assert_nil epubcfi('/6/4[chap01ref]!/4[body01]/10[para05]/3:10') <=>
48
+ epubcfi('/6/4[chap01ref]!/4[body01]/10[para05]/3!:10')
49
+ assert_equal 1, epubcfi('/6/4') <=> epubcfi('/6')
50
+ end
51
+ end
52
+
53
+ class TestRange < self
54
+ def test_attributes
55
+ parent = epubcfi('/6/4[chap01ref]!/4[body01]/10[para05]')
56
+ first = epubcfi('/6/4[chap01ref]!/4[body01]/10[para05]/2/1:1')
57
+ last = epubcfi('/6/4[chap01ref]!/4[body01]/10[para05]/3:4')
58
+ range = epubcfi('/6/4[chap01ref]!/4[body01]/10[para05],/2/1:1,/3:4')
59
+ assert_equal 0, first <=> range.first
60
+ assert_equal 0, last <=> range.last
61
+
62
+ assert_equal 0, parent <=> range.parent_path
63
+ end
64
+
65
+ def test_to_s
66
+ cfi = '/6/4[chap01ref]!/4[body01]/10[para05],/2/1:1,/3:4'
67
+ assert_equal 'epubcfi(' + cfi + ')', epubcfi('/6/4[chap01ref]!/4[body01]/10[para05],/2/1:1,/3:4').to_s
68
+ end
69
+
70
+ def test_cover
71
+ assert_true epubcfi('/6/4[chap01ref]!/4[body01]/10[para05],/2/1:1,/3:4').cover? epubcfi('/6/4[chap01ref]!/4[body01]/10[para05]/2/2/4')
72
+ end
73
+ end
74
+
75
+ class TestStep < self
76
+ def test_to_s
77
+ assert_equal '/6', EPUB::CFI::Step.new(6).to_s
78
+ assert_equal '/4[id]', EPUB::CFI::Step.new(4, EPUB::CFI::IDAssertion.new('id')).to_s
79
+ end
80
+
81
+ def test_compare
82
+ assert_equal 0, EPUB::CFI::Step.new(6) <=> EPUB::CFI::Step.new(6, 'assertion')
83
+ assert_equal -1, EPUB::CFI::Step.new(6) <=> EPUB::CFI::Step.new(7)
84
+ end
85
+ end
86
+
87
+ class TestIDAssertion < self
88
+ def test_to_s
89
+ assert_equal '[id]', EPUB::CFI::IDAssertion.new('id').to_s
90
+ assert_equal '[id;p=a]', EPUB::CFI::IDAssertion.new('id', {'p' => ['a']}).to_s
91
+ end
92
+ end
93
+
94
+ class TestTextLocationAssertion < self
95
+ def test_to_s
96
+ assert_equal '[yyy]', EPUB::CFI::TextLocationAssertion.new('yyy').to_s
97
+ assert_equal '[xx,y]', EPUB::CFI::TextLocationAssertion.new('xx', 'y').to_s
98
+ assert_equal '[,y]', EPUB::CFI::TextLocationAssertion.new(nil, 'y').to_s
99
+ assert_equal '[;s=b]', EPUB::CFI::TextLocationAssertion.new(nil, nil, {'s' => ['b']}).to_s
100
+ assert_equal '[yyy;s=b]', EPUB::CFI::TextLocationAssertion.new('yyy', nil, {'s' => ['b']}).to_s
101
+ end
102
+ end
103
+
104
+ class TestCharacterOffset < self
105
+ def test_to_s
106
+ assert_equal ':1', EPUB::CFI::CharacterOffset.new(1).to_s
107
+ assert_equal ':2[yyy]', EPUB::CFI::CharacterOffset.new(2, EPUB::CFI::TextLocationAssertion.new('yyy')).to_s
108
+ end
109
+
110
+ def test_compare
111
+ assert_equal 0,
112
+ EPUB::CFI::CharacterOffset.new(3) <=>
113
+ EPUB::CFI::CharacterOffset.new(3, EPUB::CFI::TextLocationAssertion.new('yyy'))
114
+ assert_equal -1,
115
+ EPUB::CFI::CharacterOffset.new(4) <=>
116
+ EPUB::CFI::CharacterOffset.new(5)
117
+ assert_equal 1,
118
+ EPUB::CFI::CharacterOffset.new(4, EPUB::CFI::TextLocationAssertion.new(nil, 'xx')) <=>
119
+ EPUB::CFI::CharacterOffset.new(2)
120
+ end
121
+ end
122
+
123
+ class TestSpatialOffset < self
124
+ def test_to_s
125
+ assert_equal '@0.5:30.2', EPUB::CFI::TemporalSpatialOffset.new(nil, 0.5, 30.2).to_s
126
+ assert_equal '@0:100', EPUB::CFI::TemporalSpatialOffset.new(nil, 0, 100).to_s
127
+ assert_equal '@50:50.0', EPUB::CFI::TemporalSpatialOffset.new(nil, 50, 50.0).to_s
128
+ end
129
+
130
+ def test_compare
131
+ assert_equal 0,
132
+ EPUB::CFI::TemporalSpatialOffset.new(nil, 30, 40) <=>
133
+ EPUB::CFI::TemporalSpatialOffset.new(nil, 30, 40)
134
+ assert_equal 1,
135
+ EPUB::CFI::TemporalSpatialOffset.new(nil, 30, 40) <=>
136
+ EPUB::CFI::TemporalSpatialOffset.new(nil, 40, 30)
137
+ end
138
+ end
139
+
140
+ class TestTemporalOffset < self
141
+ def test_to_s
142
+ assert_equal '~23.5', EPUB::CFI::TemporalSpatialOffset.new(23.5).to_s
143
+ end
144
+
145
+ def test_compare
146
+ assert_equal 0,
147
+ EPUB::CFI::TemporalSpatialOffset.new(23.5) <=>
148
+ EPUB::CFI::TemporalSpatialOffset.new(23.5)
149
+ assert_equal -1,
150
+ EPUB::CFI::TemporalSpatialOffset.new(23) <=>
151
+ EPUB::CFI::TemporalSpatialOffset.new(23.5)
152
+ end
153
+ end
154
+
155
+ class TestTemporalSpatialOffset < self
156
+ def test_to_s
157
+ assert_equal '~23.5@50:30.0', EPUB::CFI::TemporalSpatialOffset.new(23.5, 50, 30.0).to_s
158
+ end
159
+
160
+ def test_compare
161
+ assert_equal 0,
162
+ EPUB::CFI::TemporalSpatialOffset.new(23.5, 30, 40) <=>
163
+ EPUB::CFI::TemporalSpatialOffset.new(23.5, 30, 40.0)
164
+ assert_equal 1,
165
+ EPUB::CFI::TemporalSpatialOffset.new(23.5, 30, 40) <=>
166
+ EPUB::CFI::TemporalSpatialOffset.new(23.5)
167
+ assert_equal -1,
168
+ EPUB::CFI::TemporalSpatialOffset.new(nil, 30, 40) <=>
169
+ EPUB::CFI::TemporalSpatialOffset.new(23.5, 30, 40)
170
+ assert_equal -1,
171
+ EPUB::CFI::TemporalSpatialOffset.new(23.5, 30, 40) <=>
172
+ EPUB::CFI::TemporalSpatialOffset.new(23.5, 30, 50)
173
+ assert_equal 1,
174
+ EPUB::CFI::TemporalSpatialOffset.new(24, 30, 40) <=>
175
+ EPUB::CFI::TemporalSpatialOffset.new(23.5, 100, 100)
176
+ end
177
+ end
178
+
179
+ private
180
+
181
+ def epubcfi(string)
182
+ EPUB::CFI::Parser.new.parse(string)
183
+ end
184
+
185
+ def assert_equal_node(expected, actual, message='')
186
+ diff = AssertionMessage.delayed_diff(expected.to_s, actual.to_s)
187
+ message = build_message(message, <<EOT, expected, actual, diff)
188
+ <?>
189
+ expected but was
190
+ <?>.?
191
+ EOT
192
+ assert_block message do
193
+ expected.tdiff_equal actual
194
+ end
195
+ end
196
+ end