webtranslateit-hpricot 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +106 -0
  3. data/Gemfile +2 -0
  4. data/README.md +187 -18
  5. data/Rakefile +32 -226
  6. data/hpricot.gemspec +32 -15
  7. data/lib/hpricot/builder.rb +34 -8
  8. data/lib/hpricot/elements.rb +2 -2
  9. data/lib/hpricot/nodes.rb +153 -0
  10. data/lib/hpricot/scanner.rb +474 -0
  11. data/lib/hpricot/tag.rb +44 -6
  12. data/lib/hpricot/traverse.rb +3 -4
  13. data/lib/hpricot/tree_builder.rb +387 -0
  14. data/lib/hpricot/xs.rb +73 -0
  15. data/lib/hpricot.rb +48 -9
  16. metadata +56 -60
  17. data/.gitignore +0 -15
  18. data/ext/fast_xs/FastXsService.java +0 -1123
  19. data/ext/fast_xs/extconf.rb +0 -4
  20. data/ext/fast_xs/fast_xs.c +0 -210
  21. data/ext/hpricot_scan/HpricotCss.java +0 -850
  22. data/ext/hpricot_scan/HpricotScanService.java +0 -2085
  23. data/ext/hpricot_scan/MANIFEST +0 -0
  24. data/ext/hpricot_scan/extconf.rb +0 -9
  25. data/ext/hpricot_scan/hpricot_common.rl +0 -76
  26. data/ext/hpricot_scan/hpricot_css.c +0 -3511
  27. data/ext/hpricot_scan/hpricot_css.java.rl +0 -155
  28. data/ext/hpricot_scan/hpricot_css.rl +0 -120
  29. data/ext/hpricot_scan/hpricot_scan.c +0 -6848
  30. data/ext/hpricot_scan/hpricot_scan.h +0 -79
  31. data/ext/hpricot_scan/hpricot_scan.java.rl +0 -1173
  32. data/ext/hpricot_scan/hpricot_scan.rl +0 -911
  33. data/lib/hpricot/blankslate.rb +0 -63
  34. data/setup.rb +0 -1585
  35. data/test/files/basic.xhtml +0 -17
  36. data/test/files/boingboing.html +0 -2266
  37. data/test/files/cy0.html +0 -3653
  38. data/test/files/immob.html +0 -400
  39. data/test/files/pace_application.html +0 -1320
  40. data/test/files/tenderlove.html +0 -16
  41. data/test/files/uswebgen.html +0 -220
  42. data/test/files/utf8.html +0 -1054
  43. data/test/files/week9.html +0 -1723
  44. data/test/files/why.xml +0 -19
  45. data/test/load_files.rb +0 -7
  46. data/test/nokogiri-bench.rb +0 -64
  47. data/test/test_alter.rb +0 -96
  48. data/test/test_builder.rb +0 -37
  49. data/test/test_parser.rb +0 -496
  50. data/test/test_paths.rb +0 -25
  51. data/test/test_preserved.rb +0 -88
  52. data/test/test_xml.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2c7f0e599b62be02967d46819ec60c457e8e7c2207752ef328d069d3ca3d627
4
- data.tar.gz: 75205569719178f6b699114f54a042d491b4a3fc16248ace4a5f171460a720cf
3
+ metadata.gz: 23b27a1746fc1bbaf18618228aa7355bb333c6b6b7bef6ba3baa228c096307bc
4
+ data.tar.gz: 79c391d5c90b510b969557372e9c944fca144b3bc42241908f894ed20dcbea70
5
5
  SHA512:
6
- metadata.gz: f7a5c3f9770659390d82c477c9ec45968c560e8c878399708a4a23a93c7a61fef9b46eaea6c0a0fff12b62ffbc318e8b03fd34c8095cb3687445badbc02999a5
7
- data.tar.gz: 85afd161d4358033e9b4e32c69c35af264f88599849fc0022444f41da2cc91dade9d9949e07bf9e36980e3b613d1b5d92f1cb17253660f57b44205d9d5d16164
6
+ metadata.gz: 269e7a6e9f389c3045a5ee1f5eb824a5e99db52896b60b210d55ae51b66ac5894cebb2b3926837c3303ee6aec841289730f7e9e0ebfd653e06e94ecc0355f9ab
7
+ data.tar.gz: f0ac2d5cb9d96a7d2929e73d2ca94236fa067f1bd13832fb636eaad4638e73cac614f25ee5891cdf6bc8153545a8efea032994437adc7f065a41376e32ef1e96
data/CHANGELOG CHANGED
@@ -1,3 +1,109 @@
1
+ = 1.0.0
2
+ === 9 September 2026
3
+
4
+ This release replaces the C extension with pure Ruby. No native code is
5
+ compiled at install time and ragel is no longer needed to build or contribute.
6
+
7
+ * Added an :html_void parse option. XML has no void elements, so parsing HTML
8
+ through Hpricot::XML opened <br> as a container and everything after it became
9
+ its children -- "<p>one<br>two</p>" put "two" inside the <br>, and to_html
10
+ emitted a "</br>" that was never in the source. With html_void: true the
11
+ twelve HTML void elements (area, base, basefont, br, col, hr, img, input,
12
+ isindex, link, meta, param) take no children and get no end tag, while the
13
+ rest of XML parsing is unchanged.
14
+
15
+ The set is derived from ElementContent rather than hardcoded, so it cannot
16
+ drift from what HTML mode already does.
17
+
18
+ A void element is serialized the way the source wrote it: "<br>" stays "<br>"
19
+ and "<hr />" stays "<hr />". HTML mode is untouched and still writes the XHTML
20
+ form. Without the option a <br> element remains an ordinary element, which
21
+ matters because XLIFF, DocBook and Android string resources contain them
22
+ legitimately and dropping the end tag would produce unbalanced output.
23
+
24
+ Breaking changes
25
+ * Requires Ruby >= 3.3. Ruby 3.2 reached end of life on 31 March 2026.
26
+ * Hpricot.scan no longer accepts a block. The block form dereferenced a NULL
27
+ state pointer and segfaulted on any tag carrying an attribute; it had no
28
+ known callers.
29
+ * Hpricot.css is removed. Its scanner dropped the last token of every selector,
30
+ so "div" matched nothing and "#id.cls" silently lost ".cls". Elements#search
31
+ has always used the Ruby implementation in elements.rb and is unaffected.
32
+ * Node strings now carry the source document's encoding when it declares one,
33
+ rather than always Encoding.default_external. A binary source (File.binread,
34
+ Zip::File#read) is still tagged default_external, since its bytes carry no
35
+ encoding information and callers depend on that. Previously a Latin-1
36
+ document produced UTF-8-labelled strings that failed valid_encoding?. One
37
+ consequence: concatenating to_html output from documents of DIFFERENT
38
+ encodings now raises Encoding::CompatibilityError, where before every result
39
+ was default_external and always concatenated.
40
+ * html_quote escapes an attribute quote as &quot; rather than a backslash.
41
+ HTML does not honour backslash escaping inside an attribute value, so the old
42
+ output did not re-parse as the same element -- <span style="font-family:\"MS
43
+ Mincho\""> came back as a text node and a bogus end tag -- and a quote in an
44
+ attribute became an injection point. to_html output therefore differs from
45
+ 0.10.0 for any attribute value containing a literal quote.
46
+ * Hpricot.uxs no longer raises on hostile entity references. &#99999999999;
47
+ raised RangeError, &#xD800; produced invalid UTF-8, &#0; emitted a NUL byte,
48
+ and any non-UTF-8 input raised Encoding::CompatibilityError. Out-of-range,
49
+ surrogate and XML-invalid codepoints now become "?".
50
+ * BlankSlate is removed and Builder::CssProxy inherits from BasicObject.
51
+ BlankSlate hid nothing (its guard compared Symbols against a String, dead
52
+ since Ruby 1.9), installed an Object.method_added hook that fired for every
53
+ method definition anywhere in the host process, leaked method_added as a
54
+ public method on every class, and raised SystemStackError if loaded twice.
55
+
56
+ Fixes
57
+ * Parsing is deterministic. The C scanner read uninitialised memory in its
58
+ HTML-mode implicit-close path, so the same bytes could produce different
59
+ trees from one process to the next -- 20 runs of the test suite produced five
60
+ distinct outcomes.
61
+ * Two reachable segfaults are gone: Hpricot.scan with a block on any tag with
62
+ an attribute, and IO input whose length is an exact multiple of 16384 bytes
63
+ (or any reader returning nil).
64
+ * An unbounded leak is gone. Every exception path leaked the parser state plus
65
+ a permanently registered GC root: roughly 1.4KB retained per call, reachable
66
+ from any document containing an unknown encoding in its XML declaration.
67
+ * An unknown encoding in an XML declaration no longer raises EncodingError.
68
+ * Malformed input parses in linear time. Unquoted attribute values swallowing
69
+ "<", adjacent text-node merging, unmatched end tags and the HTML
70
+ implicit-close ancestor walk were all quadratic. The C scanner is quadratic
71
+ on the last two as well; on malformed input this release is 12-37x faster
72
+ than the extension it replaces.
73
+ * Elements#at and its % alias work again. They referenced Fixnum, removed in
74
+ Ruby 3.2, and so raised NameError for every argument type.
75
+ * Hpricot::DocType.new, Builder#doctype, #xhtml_transitional and #xhtml_strict
76
+ work again. The C node's attribute slot was nil when built from Ruby, so the
77
+ setters raised FrozenError.
78
+ * Traverse#nodes_at no longer prints to stdout.
79
+ * to_html no longer mutates frozen string literals.
80
+ * String#fast_xs is reimplemented in Ruby, byte-for-byte identical to the C
81
+ version across all 256 single-byte inputs.
82
+
83
+ Byte-identical round-tripping
84
+ * Unterminated comments, CDATA sections and processing instructions keep their
85
+ source bytes. The C scanner dropped the opening delimiter of an unterminated
86
+ comment, so "<p>a</p><!-- never closed" came back four bytes short.
87
+ * Unterminated attribute quotes round-trip; the C scanner prepended the tag
88
+ name to the resulting text node.
89
+
90
+ Other
91
+ * JRuby is supported by the same code. The separate Java scanner is gone.
92
+ * CI covers every maintained Ruby (3.3, 3.4, 4.0) on Linux and macOS, and gates
93
+ on determinism, byte-identical round-tripping and sub-quadratic scaling.
94
+ * setup.rb and the mswin32 cross-compile machinery are removed. The ragel
95
+ grammar is kept under docs/grammar/ as the specification the Ruby scanner was
96
+ ported from; the C implementation remains available at the tag
97
+ pre-pure-ruby-scanner.
98
+
99
+ = 0.10.0
100
+ === 24 March 2026
101
+ * Fix compilation with Ruby 4.
102
+ * Use HTTPS source in Gemfile (HTTP disallowed by Bundler)
103
+ * Fix incompatible function pointer types for ref_func/set_func arrays
104
+ * Remove deprecated OBJ_TAINT/OBJ_TAINTED calls (removed in Ruby 3.2+)
105
+ * Fix format string precision type and uninitialized variable in hpricot_css
106
+
1
107
  = 0.9.0
2
108
  === 23 April 2024
3
109
  * Fix issue compiling with clang 16.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/README.md CHANGED
@@ -1,20 +1,196 @@
1
- # Hpricot is over.
1
+ # webtranslateit-hpricot
2
2
 
3
- After years of lack of a proper maintainer for one of why's jewels, it has been
4
- decided to finally close the book on hpricot. Most users have migrated to alternatives
5
- and there is simply no time or energy to continue with the current codebase.
3
+ A maintained fork of [hpricot](https://github.com/hpricot/hpricot), _why the
4
+ lucky stiff's HTML/XML parser. Upstream was declared over in 2013; this fork
5
+ exists because [WebTranslateIt](https://webtranslateit.com) still depends on it
6
+ and intends to keep it working.
6
7
 
7
- If you feel that you have the time and wish to take it over, I suggest you instead
8
- think about making the hpricot-like API within nokogiri 100% compatible, that is a better
9
- use of your time.
8
+ ## Why this fork exists
10
9
 
11
- But if you still feel like "No damnit, I wanna work on hpricot itself still!" then fork
12
- this repo and start work. Send @evanphx or @nicksieger a message if you feel like you
13
- want to take over the gem name with new releases under the hpricot name.
10
+ We parse translation and localisation files `.resx`, `.xml`, `.ts`, `.tbx`,
11
+ `.stringsdict`, `.xtb`, `.docx`, and a dozen more and hand them back to
12
+ customers after editing. That imposes a requirement most parsers do not meet:
13
+
14
+ **Byte-identical round-tripping.** If a customer's file writes `&#8230;` we must
15
+ give back `&#8230;`, not `…`. If it writes `&quot;` we must give back `&quot;`,
16
+ not `"`. If it orders attributes a certain way, that order must survive. A
17
+ translation tool that silently reformats the untouched 99% of a file is worse
18
+ than useless — it turns every export into an unreviewable diff.
19
+
20
+ We measured the alternatives against a real corpus:
21
+
22
+ | Parser | Preserves `&#8230;` | Preserves `&quot;` | Preserves attribute order | Byte-identical |
23
+ |---|---|---|---|---|
24
+ | hpricot | yes | yes | yes | **yes** |
25
+ | Nokogiri | only via `encoding: 'US-ASCII'`, all-or-nothing | no | yes | no |
26
+ | Oga | no | no | yes | no |
27
+ | REXML (`raw: :all`) | yes | yes | **no** | no |
28
+
29
+ Hpricot manages this because it does not decode. It records the source byte span
30
+ of every node and emits those bytes verbatim for anything you did not modify.
31
+ Fidelity is a property of *not* re-encoding, and re-encoding is exactly what a
32
+ conformant serializer must do.
33
+
34
+ The second requirement is **liberality**. Hpricot accepts malformed markup that
35
+ a conformant parser rejects. Real uploaded files are frequently malformed, and
36
+ "your file is invalid" is not an acceptable answer when the previous version of
37
+ the product accepted it. A prior attempt to move to REXML foundered on precisely
38
+ this — it correctly rejected files hpricot tolerated.
39
+
40
+ Hpricot is also the fastest of the four we measured (~1.6x Nokogiri), though
41
+ that is the least important reason: parsing is well under half the cost of a
42
+ typical import job.
43
+
44
+ ## Current state
45
+
46
+ The library works and is in production. The Ruby layer is actively maintained.
47
+
48
+ **The C extension is being replaced with pure Ruby.** The scanner is a
49
+ ragel-generated C state machine that has had no upstream maintenance since 2013,
50
+ and a review turned up several memory-safety defects: an uninitialised read that
51
+ makes HTML-mode parsing non-deterministic across processes, two reachable
52
+ segfaults, an unbounded leak on parse errors, and GC roots registered against
53
+ dead stack frames. It also no longer compiles on Ruby trunk.
54
+
55
+ Since parsing is not the bottleneck, there is no reason to keep hand-patched C
56
+ in a library that consumes untrusted input. A pure-Ruby scanner eliminates that
57
+ entire class of defect by construction, removes the parallel Java implementation
58
+ maintained for JRuby, and means no native extension to rebuild on every Ruby
59
+ release. The plan is in
60
+ [`docs/superpowers/plans/`](docs/superpowers/plans/2026-09-08-pure-ruby-scanner.md).
61
+
62
+ Byte-identical round-tripping and liberality are the acceptance criteria for
63
+ that work, verified by a differential harness that compares the new scanner
64
+ against the old C one on a corpus of real files.
65
+
66
+ ## Installing
67
+
68
+ $ gem install webtranslateit-hpricot
69
+
70
+ Or in a Gemfile:
71
+
72
+ ```ruby
73
+ gem 'webtranslateit-hpricot'
74
+ ```
75
+
76
+ The API is hpricot's — `require 'hpricot'` and everything below applies
77
+ unchanged.
78
+
79
+ ## Performance
80
+
81
+ Measured on Ruby 4.0.6, arm64-darwin, parsing a real Android `strings.xml`
82
+ fixture scaled up by repeating its `<string>` elements. `Hpricot::XML`, mean of
83
+ several runs.
84
+
85
+ | Input | C/ragel scanner | pure Ruby | + YJIT | ratio (YJIT) |
86
+ |---|---|---|---|---|
87
+ | 26 KB | 0.4 ms | 2.3 ms | 1.8 ms | 4.5x |
88
+ | 186 KB | 4.3 ms | 22.8 ms | 15.7 ms | 3.7x |
89
+ | 1.5 MB | 45.4 ms | 190.6 ms | 151.3 ms | 3.3x |
90
+ | 5 MB | 153.2 ms | 716.5 ms | 554.4 ms | 3.6x |
91
+
92
+ Both are linear. YJIT is worth roughly 25-30% and needs no code change. Parsing
93
+ is under half the cost of even parse-plus-extract, before encoding detection,
94
+ entity decoding or database work, so it is not the bottleneck in any real
95
+ pipeline.
96
+
97
+ ### Malformed input: faster than the C scanner
98
+
99
+ The table above is well-formed input, where C wins. On malformed input the
100
+ pure-Ruby scanner is now **considerably faster**, because several quadratic
101
+ behaviours were fixed during the port that the C scanner still has:
102
+
103
+ | Input | C/ragel scanner | pure Ruby |
104
+ |---|---|---|
105
+ | 97 KB of nested `<div>` | 1.73 s | 0.062 s |
106
+ | 78 KB of `<a>` inside `<button>` | 1.58 s | 0.070 s |
107
+ | 214 KB of unmatched end tags | 2.85 s | 0.077 s |
108
+
109
+ These are not micro-optimisations: the same shapes were quadratic here too at
110
+ various points during the port, at up to 77 seconds for 156 KB. Since this
111
+ library parses untrusted uploads, an O(n²) input shape is a denial-of-service
112
+ vector rather than a slow path, so `test/test_complexity.rb` asserts the
113
+ scaling ratio for each of them and fails if any becomes quadratic again.
114
+
115
+ ### Where the time goes
116
+
117
+ Scanning is ~80% of parse time and tree building ~20%. Profiling the scanner
118
+ puts GC at 22% of samples (13.9% sweeping, 7.7% marking) and `String#byteslice`
119
+ at another 10%, so allocation pressure dominates and that is where tuning has
120
+ to aim.
121
+
122
+ What worked:
123
+
124
+ * Dispatching on the first byte, so a text token does not attempt four
125
+ construct regexes before falling through.
126
+ * `StringScanner#skip` instead of `#scan` wherever the matched text is
127
+ discarded — `scan` allocates a String for the match even when only its
128
+ success matters.
129
+ * Matching a whole attribute in one scan rather than six calls, using the
130
+ capture groups directly instead of separate byteslices (~5%).
131
+ * Not copying the document. A source already valid in an ASCII-compatible
132
+ encoding is scanned in place: `StringScanner#pos` and `#skip` are
133
+ byte-oriented whatever the encoding, and the character classes here are all
134
+ ASCII-only. This removes the copies that used to be made per parse — two for
135
+ a BINARY source (the scanning buffer, plus a re-encoded copy to slice from),
136
+ one for a source carrying a declared encoding. `File.binread` and
137
+ `Zip::File#read` both give BINARY, so the two-copy case is the common one in
138
+ practice, and a 5 MB upload stops allocating 10 MB of copies. Peak RSS is
139
+ dominated by the parse tree, so this does not show up there; it is an
140
+ allocation win, not a footprint one.
141
+
142
+ What did not work, measured and reverted: interning repeated tag names and
143
+ attribute keys. `string` and `name` recur thousands of times in a translation
144
+ file, but the lookup still allocates the temporary it searches with, so the
145
+ allocation count was unchanged and the hash probe cost as much as it saved.
146
+
147
+ ### Why this is viable now and was not in 2006
148
+
149
+ Hpricot was written in 2006 against Ruby 1.8, a tree-walking interpreter with
150
+ no bytecode VM. A pure-Ruby scanner then would have been perhaps an order of
151
+ magnitude slower than this one, which is a large part of why the scanner was
152
+ written in C to begin with. Ruby 1.9 brought YARV, 2.x brought generational and
153
+ incremental GC, and 3.x brought YJIT. The trade-off that justified a C extension
154
+ in 2006 simply does not hold in 2026: this scanner parses a 1.5 MB document in
155
+ 130 ms, which is comfortably faster in absolute terms than the original C
156
+ extension managed on the hardware it was written for.
157
+
158
+ ### "Pure Ruby" — what that does and does not mean
159
+
160
+ The hot loop runs inside `StringScanner#scan`, and `strscan` is implemented in C
161
+ in CRuby. So C still executes; what changed is *whose* C it is.
162
+
163
+ What was removed is this project's own native code: roughly 7,000 lines of
164
+ ragel-generated C plus a hand-written scanner, a parallel Java implementation
165
+ for JRuby, and `fast_xs`. That code had no upstream maintainer after 2013, could
166
+ not be regenerated without ragel (which nobody had installed), had to be
167
+ recompiled for every Ruby and platform, and had been hand-patched in the
168
+ generated output. A review of it found an uninitialised read, two reachable
169
+ segfaults, an unbounded leak and GC roots registered on dead stack frames. It
170
+ also stopped compiling on Ruby trunk.
171
+
172
+ `strscan` is a different proposition: it ships with Ruby, is maintained and
173
+ fuzzed by ruby-core, gets security fixes without us doing anything, and has an
174
+ implementation on every engine — which is why JRuby now works from the same
175
+ source with no Java of ours.
176
+
177
+ So the accurate claim is **no native extension of our own**: nothing is
178
+ compiled at install time, there is no `extensions` entry in the gemspec, no
179
+ ragel, and no platform-specific builds. A character-by-character Ruby loop
180
+ would be an order of magnitude slower than `StringScanner`, so keeping the hot
181
+ loop inside it is a deliberate design constraint.
182
+
183
+ ## Contributing
184
+
185
+ Issues and pull requests are welcome, though be aware this fork is maintained
186
+ for a specific purpose and changes are weighed against that. If you want a
187
+ general-purpose HTML parser, use
188
+ [Nokogiri](https://github.com/sparklemotion/nokogiri) — it is better maintained,
189
+ standards-compliant, and almost certainly what you want.
14
190
 
15
191
  Thanks to \_why for all the fun. We'll never forget it.
16
192
 
17
- ## Now back to your original README content...
193
+ ---
18
194
 
19
195
 
20
196
  # Hpricot, Read Any HTML
@@ -62,13 +238,6 @@ not going to say "Use at your own risk" because I don't want this library to be
62
238
  risky. If you trip on something, I'll share the liability by repairing things
63
239
  as quickly as I can. Your responsibility is to report the inadequacies.
64
240
 
65
- ## Installing Hpricot
66
-
67
- You may get the latest stable version from Rubyforge. Win32 binaries,
68
- Java binaries (for JRuby), and source gems are available.
69
-
70
- $ gem install hpricot
71
-
72
241
  ## An Hpricot Showcase
73
242
 
74
243
  We're going to run through a big pile of examples to get you jump-started.
data/Rakefile CHANGED
@@ -1,237 +1,43 @@
1
- require 'bundler/setup'
2
- ENV.delete('RUBYOPT') # Don't propagate RUBYOPT/Bundler to subprocesses
3
- require 'rake/clean'
4
- require 'rubygems/package_task'
5
- require 'rdoc/task'
6
1
  require 'rake/testtask'
7
- begin
8
- require 'rake/extensiontask'
9
- rescue LoadError
10
- abort "To build, please first gem install rake-compiler"
11
- end
12
-
13
- RbConfig = Config unless defined?(RbConfig)
14
-
15
- NAME = "hpricot"
16
- REV = (`#{ENV['GIT'] || "git"} rev-list HEAD`.split.length + 1).to_s
17
- VERS = ENV['VERSION'] || "0.8" + (REV ? ".#{REV}" : "")
18
- PKG = "#{NAME}-#{VERS}"
19
- BIN = "*.{bundle,jar,so,o,obj,pdb,lib,def,exp,class,rbc}"
20
- CLEAN.include ["#{BIN}", "ext/**/#{BIN}", "lib/**/#{BIN}", "test/**/#{BIN}",
21
- 'ext/fast_xs/Makefile', 'ext/hpricot_scan/Makefile',
22
- '**/.*.sw?', '*.gem', '.config', 'pkg', 'lib/hpricot_scan.rb', 'lib/fast_xs.rb']
23
- RDOC_OPTS = ['--quiet', '--title', 'The Hpricot Reference', '--main', 'README.md', '--inline-source']
24
- PKG_FILES = %w(CHANGELOG COPYING README.md Rakefile) +
25
- Dir.glob("{bin,doc,test,extras}/**/*") +
26
- (Dir.glob("lib/**/*.rb") - %w(lib/hpricot_scan.rb lib/fast_xs.rb)) +
27
- Dir.glob("ext/**/*.{h,java,c,rb,rl}") +
28
- %w[ext/hpricot_scan/hpricot_scan.c ext/hpricot_scan/hpricot_css.c ext/hpricot_scan/HpricotScanService.java] # needed because they are generated later
29
- RAGEL_C_CODE_GENERATION_STYLES = {
30
- "table_driven" => 'T0',
31
- "faster_table_driven" => 'T1',
32
- "flat_table_driven" => 'F0',
33
- "faster_flat_table_driven" => 'F1',
34
- "goto_driven" => 'G0',
35
- "faster_goto_driven" => 'G1',
36
- "really_fast goto_driven" => 'G2'
37
- # "n_way_split_really_fast_goto_driven" => 'P<N>'
38
- }
39
- DEFAULT_RAGEL_C_CODE_GENERATION = "really_fast goto_driven"
40
- SPEC =
41
- Gem::Specification.new do |s|
42
- s.name = NAME
43
- s.version = VERS
44
- s.platform = Gem::Platform::RUBY
45
- s.has_rdoc = true
46
- s.rdoc_options += RDOC_OPTS
47
- s.extra_rdoc_files = ["README.md", "CHANGELOG", "COPYING"]
48
- s.summary = "a swift, liberal HTML parser with a fantastic library"
49
- s.description = s.summary
50
- s.author = "why the lucky stiff"
51
- s.email = 'why@ruby-lang.org'
52
- s.homepage = 'http://code.whytheluckystiff.net/hpricot/'
53
- s.rubyforge_project = 'hobix'
54
- s.files = PKG_FILES
55
- s.require_paths = ["lib"]
56
- s.extensions = FileList["ext/**/extconf.rb"].to_a
57
- s.bindir = "bin"
58
- end
59
- # Dup the spec before any of its calculated ivars are set (e.g., #cache_file)
60
- Win32Spec = SPEC.dup
61
- JRubySpec = SPEC.dup
62
-
63
- # FAT cross-compile
64
- # Pass RUBY_CC_VERSION=1.8.7:1.9.2 when packaging for 1.8+1.9 mswin32 binaries
65
- %w(hpricot_scan fast_xs).each do |target|
66
- Rake::ExtensionTask.new(target, SPEC) do |ext|
67
- ext.lib_dir = File.join('lib', target) if ENV['RUBY_CC_VERSION']
68
- ext.cross_compile = true # enable cross compilation (requires cross compile toolchain)
69
- ext.cross_platform = 'i386-mswin32' # forces the Windows platform instead of the default one
70
- end
71
-
72
- # HACK around 1.9.2 cross .def file creation
73
- def_file = "tmp/i386-mswin32/#{target}/1.9.2/#{target}-i386-mingw32.def"
74
- directory File.dirname(def_file)
75
- file def_file => File.dirname(def_file) do |t|
76
- File.open(t.name, "w") do |f|
77
- f << "EXPORTS\nInit_#{target}\n"
78
- end
79
- end
80
-
81
- task File.join(File.dirname(def_file), "Makefile") => def_file
82
- # END HACK
83
- file "lib/#{target}.rb" do |t|
84
- File.open(t.name, "w") do |f|
85
- f.puts %{require "#{target}/\#{RUBY_VERSION.sub(/\\.\\d+$/, '')}/#{target}"}
86
- end
87
- end
88
- end
89
- file 'ext/hpricot_scan/extconf.rb' => :ragel
90
-
91
- desc "set environment variables to build and/or test with debug options"
92
- task :debug do
93
- ENV['CFLAGS'] ||= ""
94
- ENV['CFLAGS'] += " -g -DDEBUG"
95
- end
96
-
97
- desc "Does a full compile, test run"
98
- if defined?(JRUBY_VERSION)
99
- task :default => [:compile_java, :clean_fat_rb, :test]
100
- else
101
- task :default => [:compile, :clean_fat_rb, :test]
102
- end
103
-
104
- task :clean_fat_rb do
105
- rm_f "lib/hpricot_scan.rb"
106
- rm_f "lib/fast_xs.rb"
107
- end
108
-
109
- desc "Packages up Hpricot for all platforms."
110
- task :package => [:clean]
111
2
 
112
- desc "Run all the tests"
113
- Rake::TestTask.new do |t|
114
- t.libs << "test"
115
- t.test_files = FileList['test/test_*.rb']
116
- t.verbose = true
3
+ # test_differential needs a separately built checkout of the old C scanner as
4
+ # an oracle (see test/differential_helper.rb); it is a development tool, not
5
+ # part of the default suite.
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs = %w[lib test]
8
+ t.test_files = FileList['test/test_*.rb'].exclude(/differential/)
9
+ t.verbose = true
117
10
  end
118
11
 
119
- Rake::RDocTask.new do |rdoc|
120
- rdoc.rdoc_dir = 'doc/rdoc'
121
- rdoc.options += RDOC_OPTS
122
- rdoc.main = "README.md"
123
- rdoc.rdoc_files.add ['README.md', 'CHANGELOG', 'COPYING', 'lib/**/*.rb']
124
- end
125
-
126
- Gem::PackageTask.new(SPEC) do |p|
127
- p.need_tar = true
128
- p.gem_spec = SPEC
129
- end
130
-
131
- ### Win32 Packages ###
132
- Win32Spec.platform = 'i386-mswin32'
133
- Win32Spec.files = PKG_FILES + %w(hpricot_scan fast_xs).map do |t|
134
- unless ENV['RUBY_CC_VERSION']
135
- file "lib/#{t}/1.8/#{t}.so" do
136
- abort "ERROR while packaging: re-run for fat win32 gems:\nrake #{ARGV.join(' ')} RUBY_CC_VERSION=1.8.7:1.9.2"
137
- end
138
- end
139
- ["lib/#{t}.rb", "lib/#{t}/1.8/#{t}.so", "lib/#{t}/1.9/#{t}.so"]
140
- end.flatten
141
- Win32Spec.extensions = []
142
-
143
- Gem::PackageTask.new(Win32Spec) do |p|
144
- p.need_tar = false
145
- p.gem_spec = Win32Spec
12
+ Rake::TestTask.new(:differential) do |t|
13
+ t.libs = %w[lib test]
14
+ t.test_files = FileList['test/test_differential.rb']
15
+ t.verbose = true
146
16
  end
147
17
 
148
- JRubySpec.platform = 'java'
149
- JRubySpec.files = PKG_FILES + ["lib/hpricot_scan.jar", "lib/fast_xs.jar"]
150
- JRubySpec.extensions = []
151
-
152
- Gem::PackageTask.new(JRubySpec) do |p|
153
- p.need_tar = false
154
- p.gem_spec = JRubySpec
155
- end
156
-
157
- desc "Determines the Ragel version and displays it on the console along with the location of the Ragel binary."
158
- task :ragel_version do
159
- @ragel_v = `ragel -v`[/(version )(\S*)/,2].to_f
160
- puts "Using ragel version: #{@ragel_v}, location: #{`which ragel`}"
161
- @ragel_v
162
- end
163
-
164
- desc "Generates the C scanner code with Ragel."
165
- task :ragel => [:ragel_version] do
166
- if @ragel_v >= 6.1
167
- @ragel_c_code_generation_style = RAGEL_C_CODE_GENERATION_STYLES[DEFAULT_RAGEL_C_CODE_GENERATION]
168
- Dir.chdir("ext/hpricot_scan") do
169
- sh %{ragel hpricot_scan.rl -#{@ragel_c_code_generation_style} -o hpricot_scan.c}
170
- sh %{ragel hpricot_css.rl -#{@ragel_c_code_generation_style} -o hpricot_css.c}
171
- end
172
- else
173
- STDERR.puts "Ragel 6.1 or greater is required."
174
- exit(1)
175
- end
176
- end
177
-
178
- # Java only supports the table-driven code
179
- # generation style at this point.
180
- desc "Generates the Java scanner code using the Ragel table-driven code generation style."
181
- task :ragel_java => [:ragel_version] do
182
- if @ragel_v >= 6.1
183
- puts "compiling with ragel version #{@ragel_v}"
184
- Dir.chdir("ext/hpricot_scan") do
185
- sh %{ragel -J -o HpricotCss.java hpricot_css.java.rl}
186
- sh %{ragel -J -o HpricotScanService.java hpricot_scan.java.rl}
187
- end
188
- else
189
- STDERR.puts "Ragel 6.1 or greater is required."
190
- exit(1)
191
- end
192
- end
193
-
194
- ### JRuby Compile ###
195
-
196
- def java_classpath_arg # myriad of ways to discover JRuby classpath
197
- begin
198
- cpath = Java::java.lang.System.getProperty('java.class.path').split(File::PATH_SEPARATOR)
199
- cpath += Java::java.lang.System.getProperty('sun.boot.class.path').split(File::PATH_SEPARATOR)
200
- jruby_cpath = cpath.compact.join(File::PATH_SEPARATOR)
201
- rescue => e
202
- end
203
- unless jruby_cpath
204
- jruby_cpath = ENV['JRUBY_PARENT_CLASSPATH'] || ENV['JRUBY_HOME'] &&
205
- FileList["#{ENV['JRUBY_HOME']}/lib/*.jar"].join(File::PATH_SEPARATOR)
206
- end
207
- unless jruby_cpath || ENV['CLASSPATH'] =~ /jruby/
208
- abort %{WARNING: No JRuby classpath has been set up.
209
- Define JRUBY_HOME=/path/to/jruby on the command line or in the environment}
210
- end
211
- "-cp \"#{jruby_cpath}\""
212
- end
213
-
214
- def compile_java(filenames, jarname)
215
- sh %{javac -source 1.5 -target 1.5 #{java_classpath_arg} #{filenames.join(" ")}}
216
- sh %{jar cf #{jarname} *.class}
217
- end
218
-
219
- task :hpricot_scan_java => [:ragel_java] do
220
- Dir.chdir "ext/hpricot_scan" do
221
- compile_java(["HpricotScanService.java", "HpricotCss.java"], "hpricot_scan.jar")
222
- end
223
- end
224
-
225
- task :fast_xs_java do
226
- Dir.chdir "ext/fast_xs" do
227
- compile_java(["FastXsService.java"], "fast_xs.jar")
228
- end
18
+ desc 'Assert the suite produces identical results across 20 runs'
19
+ task :determinism do
20
+ results = 20.times.map do
21
+ `ruby -Ilib -Itest test/test_parser.rb 2>&1`[/\d+ failures, \d+ errors/]
22
+ end.tally
23
+ abort "non-deterministic: #{results.inspect}" if results.size != 1
24
+ puts "deterministic across 20 runs: #{results.keys.first}"
229
25
  end
230
26
 
231
- %w(hpricot_scan fast_xs).each do |ext|
232
- file "lib/#{ext}.jar" => "#{ext}_java" do |t|
233
- mv "ext/#{ext}/#{ext}.jar", "lib"
27
+ desc 'Assert every fixture round-trips byte-identically'
28
+ task :fidelity do
29
+ $LOAD_PATH.unshift('lib')
30
+ require 'hpricot'
31
+ files = Dir['test/files/*'].select { |f| File.file?(f) }
32
+ # Compared as bytes: src comes from binread (ASCII-8BIT) while output
33
+ # carries the document's encoding, and String#== is false across
34
+ # incompatible encodings even when the bytes match.
35
+ bad = files.reject do |f|
36
+ src = File.binread(f)
37
+ Hpricot::XML(src).to_original_html.b == src.b
234
38
  end
235
- task :compile_java => "lib/#{ext}.jar"
39
+ abort "not byte-identical: #{bad.inspect}" unless bad.empty?
40
+ puts "#{files.size} fixtures round-trip byte-identically"
236
41
  end
237
42
 
43
+ task default: %i[test fidelity determinism]