Lazy decoding of XML/HTML entities.

Instead of decoding entities in the lexer we'll do this whenever XML::Text#text
is called. This removes the overhead from the parsing phase and ensures the
process is only triggered when actually needed. Note that calling #to_xml and/or
the #inspect methods on a Text (or parent) instance will also trigger the entity
conversion process.

The new entity decoding API supports both regular entities (e.g. &) as well
as codepoint based entities (both regular and hexadecimal codepoints).

To allow safe read-only access to Text instances from multiple threads a mutex
is used. This mutex ensures that only 1 thread can trigger the conversion
process.

Fixes #68
This commit is contained in:
Yorick Peterse committed 2015-03-05 23:00:43 +01:00
1 parent 7409257702
commit 2ec91f130f
9 files changed
+2337 -76

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
# encoding: utf-8
require 'spec_helper'
describe Oga::HTML::Entities do
describe 'decode' do
it 'decodes & into &' do
described_class.decode('&').should == '&'
end
it 'decodes λ into λ' do
described_class.decode('λ').should == 'λ'
end
end
end
+4
View File
@@ -65,6 +65,10 @@ describe Oga::XML::Entities do
it 'decodes &amp;&amp;lt; into &<' do
described_class.decode('&amp;&amp;lt;').should == '&&lt;'
end
it 'decodes &#x3C; into <' do
described_class.decode('&#x3C;').should == '<'
end
end
describe 'encode' do
-55
View File
@@ -1,55 +0,0 @@
require 'spec_helper'
describe Oga::XML::Lexer do
describe 'converting XML entities in text tokens' do
it 'converts &amp; into &' do
lex('&amp;').should == [[:T_TEXT, '&', 1]]
end
it 'converts &lt; into <' do
lex('&lt;').should == [[:T_TEXT, '<', 1]]
end
it 'converts &gt; into >' do
lex('&gt;').should == [[:T_TEXT, '>', 1]]
end
end
describe 'converting XML entities in string tokens' do
it 'converts &amp; into &' do
lex('<foo class="&amp;" />').should == [
[:T_ELEM_START, nil, 1],
[:T_ELEM_NAME, 'foo', 1],
[:T_ATTR, 'class', 1],
[:T_STRING_DQUOTE, nil, 1],
[:T_STRING_BODY, '&', 1],
[:T_STRING_DQUOTE, nil, 1],
[:T_ELEM_END, nil, 1]
]
end
it 'converts &lt; into <' do
lex('<foo class="&lt;" />').should == [
[:T_ELEM_START, nil, 1],
[:T_ELEM_NAME, 'foo', 1],
[:T_ATTR, 'class', 1],
[:T_STRING_DQUOTE, nil, 1],
[:T_STRING_BODY, '<', 1],
[:T_STRING_DQUOTE, nil, 1],
[:T_ELEM_END, nil, 1]
]
end
it 'converts &gt; into >' do
lex('<foo class="&gt;" />').should == [
[:T_ELEM_START, nil, 1],
[:T_ELEM_NAME, 'foo', 1],
[:T_ATTR, 'class', 1],
[:T_STRING_DQUOTE, nil, 1],
[:T_STRING_BODY, '>', 1],
[:T_STRING_DQUOTE, nil, 1],
[:T_ELEM_END, nil, 1]
]
end
end
end
+73
View File
@@ -14,6 +14,79 @@ describe Oga::XML::Text do
end
end
describe '#text' do
describe 'with XML entities' do
it 'converts &amp; to &' do
described_class.new(:text => '&amp;').text.should == '&'
end
it 'converts &lt; to <' do
described_class.new(:text => '&lt;').text.should == '<'
end
it 'converts &gt; to >' do
described_class.new(:text => '&gt;').text.should == '>'
end
it 'caches the converted text' do
node = described_class.new(:text => '&amp;')
Oga::XML::Entities.should_receive(:decode).once.and_call_original
node.text.should == '&'
node.text.should == '&'
end
it 'converts new text set using text=' do
node = described_class.new(:text => '&amp;')
node.text.should == '&'
node.text = '&lt;'
node.text.should == '<'
end
end
describe 'with HTML entities' do
before do
@document = Oga::XML::Document.new(:type => :html)
end
it 'converts &amp; to &' do
node = described_class.new(:text => '&amp;')
@document.children << node
node.text.should == '&'
end
it 'converts &lt; to <' do
node = described_class.new(:text => '&lt;')
@document.children << node
node.text.should == '<'
end
it 'converts &gt; to >' do
node = described_class.new(:text => '&gt;')
@document.children << node
node.text.should == '>'
end
it 'converts &nbsp; into a space' do
node = described_class.new(:text => '&nbsp;')
@document.children << node
node.text.should == [160].pack('U')
end
end
end
describe '#to_xml' do
it 'generates the corresponding XML' do
node = described_class.new(:text => 'foo')