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.
- checksums.yaml +4 -4
- data/CHANGELOG +106 -0
- data/Gemfile +2 -0
- data/README.md +187 -18
- data/Rakefile +32 -226
- data/hpricot.gemspec +32 -15
- data/lib/hpricot/builder.rb +34 -8
- data/lib/hpricot/elements.rb +2 -2
- data/lib/hpricot/nodes.rb +153 -0
- data/lib/hpricot/scanner.rb +474 -0
- data/lib/hpricot/tag.rb +44 -6
- data/lib/hpricot/traverse.rb +3 -4
- data/lib/hpricot/tree_builder.rb +387 -0
- data/lib/hpricot/xs.rb +73 -0
- data/lib/hpricot.rb +48 -9
- metadata +56 -60
- data/.gitignore +0 -15
- data/ext/fast_xs/FastXsService.java +0 -1123
- data/ext/fast_xs/extconf.rb +0 -4
- data/ext/fast_xs/fast_xs.c +0 -210
- data/ext/hpricot_scan/HpricotCss.java +0 -850
- data/ext/hpricot_scan/HpricotScanService.java +0 -2085
- data/ext/hpricot_scan/MANIFEST +0 -0
- data/ext/hpricot_scan/extconf.rb +0 -9
- data/ext/hpricot_scan/hpricot_common.rl +0 -76
- data/ext/hpricot_scan/hpricot_css.c +0 -3511
- data/ext/hpricot_scan/hpricot_css.java.rl +0 -155
- data/ext/hpricot_scan/hpricot_css.rl +0 -120
- data/ext/hpricot_scan/hpricot_scan.c +0 -6848
- data/ext/hpricot_scan/hpricot_scan.h +0 -79
- data/ext/hpricot_scan/hpricot_scan.java.rl +0 -1173
- data/ext/hpricot_scan/hpricot_scan.rl +0 -911
- data/lib/hpricot/blankslate.rb +0 -63
- data/setup.rb +0 -1585
- data/test/files/basic.xhtml +0 -17
- data/test/files/boingboing.html +0 -2266
- data/test/files/cy0.html +0 -3653
- data/test/files/immob.html +0 -400
- data/test/files/pace_application.html +0 -1320
- data/test/files/tenderlove.html +0 -16
- data/test/files/uswebgen.html +0 -220
- data/test/files/utf8.html +0 -1054
- data/test/files/week9.html +0 -1723
- data/test/files/why.xml +0 -19
- data/test/load_files.rb +0 -7
- data/test/nokogiri-bench.rb +0 -64
- data/test/test_alter.rb +0 -96
- data/test/test_builder.rb +0 -37
- data/test/test_parser.rb +0 -496
- data/test/test_paths.rb +0 -25
- data/test/test_preserved.rb +0 -88
- data/test/test_xml.rb +0 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23b27a1746fc1bbaf18618228aa7355bb333c6b6b7bef6ba3baa228c096307bc
|
|
4
|
+
data.tar.gz: 79c391d5c90b510b969557372e9c944fca144b3bc42241908f894ed20dcbea70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 " 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. �
|
|
47
|
+
raised RangeError, � produced invalid UTF-8, � 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
data/README.md
CHANGED
|
@@ -1,20 +1,196 @@
|
|
|
1
|
-
#
|
|
1
|
+
# webtranslateit-hpricot
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 `…` we must
|
|
15
|
+
give back `…`, not `…`. If it writes `"` we must give back `"`,
|
|
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 `…` | Preserves `"` | 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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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::
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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]
|