Commit Graph

580 Commits

Author SHA1 Message Date
Yorick Peterse 116b9b0ceb
Make XmlDeclaration a ProcessingInstruction
This allows Oga to parse documents that contain an XML declaration at a
place other than at the document root. Oga still only assigns the XML
declaration to the document whenever it is at the top-level. This
matches libxml/XML specification behaviour as far as I can tell.
2016-09-17 14:39:07 +02:00
Scott Wheeler d40baf0c72 Add aliases for accessing attributes via [] and []=
This also fixes accessing attributes via symbol name and tests to ensure
that such does not break in the future.
2016-09-14 15:21:46 +03:00
Yorick Peterse 38284278d5
Don't process siblings when reaching a root node
When generating XML we should not process the siblings of a root node.
Doing so results in invalid XML being returned (due to siblings not
being children of the root node).

Not processing the siblings in this case also prevents the siblings loop
from getting stuck. To explain what's happening, let's assume we're
using the following document tree:

    Document
      |_ Text
      |_ Element

Now let's say we take the Text node and call "to_xml" on it. When we
start the loop we'll run into the following code:

    if child_node = children && current.children[0]
      current = child_node
    else

Here the if statement will evaluate to false because a Text node doesn't
have any child nodes, as such we enter the else branch. We now reach the
following code:

    until next_node = current.is_a?(Node) && current.next

A Text node is a descendant of Node and it happens to have another node
(the Element node) as the next sibling. As a result we enter the `until`
loop's body. We now run into this code:

    if current.is_a?(Node) && current != @start
      current = current.parent
    end

Here `current` is still our Text node and it is the @start node. As a
result the `current` re-assignment won't be evaluated.

Next we run into the following:

    after_element(current, output) if current.is_a?(Element)

    break if current == @start

The first line will not evaluate because `current` is still the `Text`
node.  The `break` *will* evaluate because `current` is the same as
@start.

This will then lead to the following code being executed:

    current = next_node

Here `next_node` is the next sibling of the Text node, which in the
above example is the Element node.

Because all of the above runs in a `while` loop we'll at some point end
up again at the start of the `until` loop. At this point the `current`
variable contains an `Element`. Because this node does *not* have a node
following it we'll once again enter the `until` loop's body.

This loop will now get stuck because `current` is a Node, it's not the
same as @start, thus `current` is set to its parent (the Document),
which also isn't the same as @start.

On the next iteration this loop will break because `current` is no
longer a node. However, because a Document _does_ have child nodes the
whole process of traversing children/siblings will keep repeating itself
forever.

To work around this we now use the following statement:

    if child_node = children && current.children[0]
      ...
    elsif current == @start
      after_element(current, output) if current.is_a?(Element)

      break
    else
      until next_node = current.is_a?(Node) && current.next
      ...
    end

This prevents processing of any siblings once we have reached the root
node, in turn preventing the loop getting stuck forever.

I'm willing to bet there are probably a few more edge cases, but I can't
think of any others at the moment.

Fixes #161
2016-09-10 02:49:05 +02:00
Yorick Peterse 68f1f9f660
Relax parsing of XML doctypes
This allows the parser to parse doctypes that contain a mixture of
names, public IDs, inline rules, etc.

Fixes #159
2016-09-06 22:25:22 +02:00
Yorick Peterse 5a58b14137
Use static variables for Node#previous/#next
Instead of calculating the previous/next node on the fly this data is
now set automatically whenever a node is stored in a NodeSet with an
owner. While this introduces some overhead and complexity when adding or
removing nodes from a NodeSet, it greatly reduces the runtime overhead
of calling Node#previous or Node#next.
2016-09-04 21:07:35 +02:00
Yorick Peterse dd138981f6
Generate XML without relying on recursion
While using recursion is an easy way of generating XML it can lead to
the call stack overflowing when serialising documents with lots of
nested nodes.

Generally there are two ways of working around this:

1. Use an explicit stack (e.g. an array or a queue of sorts) instead of
   relying on the call stack.
2. Use an algorithm that doesn't use a stack at all (e.g. Morris
   traversal).

This commit introduces the XML::Generator class which can serialize
documents back to XML without using a stack at all. This class takes
advantage of XML nodes having access to not only their child nodes, but
also their siblings and their parents.

All XML serialisation logic now resides in the XML::Generator class. In
turn the various "to_xml" methods just use this class and serialize
everything starting at "self".
2016-09-04 19:19:00 +02:00
Yorick Peterse 9ac16e2e4f
Fixed index check in Node#next
An index can/should never be equal the length of a NodeSet, thus we
should use "<" here instead of "<=".
2016-09-03 23:56:55 +02:00
Erik Michaels-Ober 3a89dcffab
Remove Parser#reset and PullParser#reset 2016-07-13 17:19:42 +02:00
Erik Michaels-Ober dc30b8b6c1
Remove Lexer#reset method
Resolves https://github.com/YorickPeterse/oga/issues/153.
2016-07-13 17:19:42 +02:00
Yorick Peterse 6d3c5c2ce9 XPath support for nested pipe operators
Basically this will process the left-hand side first, assign the result
to a variable and then append this set with the nodes from the
right-hand side.

Fixes #149
2016-02-23 22:24:07 +01:00
Yorick Peterse 5bfc2d50f2 Preserve entities that can't be decoded
Certain entities when decoded will produce a String with an invalid
encoding. This commit ensures that instead of raising an EncodingError
further down the line (e.g. when calling "inspect" on a document) the
entities are preserved as-is.

Fixes #143
2016-02-09 19:51:53 +01:00
Yorick Peterse 66fc4b1dfc Fixed parsing HTML identifiers containing colons
HTML identifiers containing colons should be treated in two ways:

* For element names the prefix (= the namespace prefix in case of XML)
  should be ignored as HTML doesn't support/use namespaces.
* For attribute names a colon is a valid character, thus "foo:bar:baz"
  should be treated as a single attribute name.

This fixes #142.
2015-12-26 20:28:35 +01:00
Yorick Peterse bd48dc15cc Evaluate compiled blocks in an isolated Binding
Re-using the Binding of the XPath::Compiler#compile method would lead to
race conditions, and possibly a memory leak due to the Binding sticking
around for compiled Proc's lifetime.

By using a dedicated class (and its corresponding Binding) we can work
around this. Access to this class is not synchronized as compiled Procs
don't mutate their enclosing environment.

The race condition can be demonstrated using code such as the
following:

    xml = <<-EOF
    <people>
      <person>
        <name>Alice</name>
      </person>

      <person>
        <name>Bob</name>
      </person>

      <person>
        <name>Eve</name>
      </person>
    </people>
    EOF

    4.times.map do
      Thread.new do
        10_000.times do
          document = Oga.parse_xml(xml)

          document.at_xpath('people/person/name').text
        end
      end
    end.each(&:join)

Running this code would result in NoMethodErrors due to "at_xpath"
returning a NilClass opposed to an Oga::XML::Element.
2015-09-07 14:02:31 +02:00
Yorick Peterse c713f6250f Lexer/parser specs for CSS axes without whitespace 2015-09-04 15:13:38 +02:00
Yorick Peterse 08bc23905e Specs for lexing CSS operators with whitespace 2015-09-04 15:08:26 +02:00
Yorick Peterse f5425b07e0 Added magic encoding comments for Ruby 1.9 2015-09-03 11:31:02 +02:00
Yorick Peterse 37c5b819fa Unicode support for CSS/XPath
Fixes #140
2015-09-03 11:21:45 +02:00
Yorick Peterse 44630c27ff Support escaping dots in CSS identifiers
Escaping hash characters and whitespace is _not_ supported as neither
are valid element/attribute names (e.g. <foo#bar /> is invalid
XML/HTML).

Escaping single/double quotes also won't be supported for the time
being. It's quite a pain to get this to work right in not just CSS but
also XPath and XML/HTML, for very little gain. Should there be enough
users with an actual use case (other than "But the spec says ...!") I'll
look into this again.

Fixes #124
2015-09-02 20:18:52 +02:00
Yorick Peterse aef7c510c2 Basic support for the CSS :not pseudo class
This does _not_ support element states such as DISABLED, nor does it
support the special handling of namespaces (e.g. *|*:not(*)). Instead
this selector basically acts as a negation, some examples:

    :not(foo)  # All but any "foo" nodes
    :not(#foo) # Skips nodes with id="foo"
    :not(.foo) # Skips nodes with a class "foo"

Fixes #125
2015-09-01 22:05:46 +02:00
Yorick Peterse 8b2455679f Revamp a few more XPath compiler specs 2015-08-31 09:39:33 +02:00
Yorick Peterse 604d0d9337 Case insensitive matching of nodes
This re-applies the patch added in #134 to the new XPath compiler.

Fixes #135.
2015-08-30 18:30:04 +02:00
Yorick Peterse bb8b328f5e Revamp compiler specs for regular paths 2015-08-30 18:26:52 +02:00
Yorick Peterse b74f8dc1a3 Removed compiler arity spec
This spec isn't very useful and breaks on 1.9 due to it apparently
handling arity values differently.
2015-08-30 01:55:31 +02:00
Yorick Peterse 1b62dd3256 Revamped compiler type test specs 2015-08-30 01:45:51 +02:00
Yorick Peterse c6df73d031 Fix eq spec to not depend on at_xpath 2015-08-30 01:20:08 +02:00
Yorick Peterse 4ad4b89860 Revamp compiler specs for "self"
This also includes a fix for node() so that it matches attributes.
2015-08-28 16:57:24 +02:00
Yorick Peterse 3a04b1da06 Root element spec for "preceding-sibling" 2015-08-28 16:50:34 +02:00
Yorick Peterse e8377b360a Revamp compiler "preceding" specs
This also includes some fixes to make this axis behave correctly when
evaluate relative to a document.
2015-08-28 16:49:59 +02:00
Yorick Peterse 6b2874c507 Revamped compiler "preceding-sibling" specs 2015-08-28 16:30:26 +02:00
Yorick Peterse 84a9315b24 Revamped compiler specs for "parent" 2015-08-28 16:22:49 +02:00
Yorick Peterse 07658dadb1 Added Attribute#parent 2015-08-28 16:22:42 +02:00
Yorick Peterse d0177633f8 Revamp namespace compiler specs 2015-08-28 15:58:42 +02:00
Yorick Peterse a1e7d2d07f Revamp compiler "following" specs 2015-08-28 15:53:57 +02:00
Yorick Peterse 824c897467 Revamp compiler specs for following-sibling 2015-08-28 15:48:03 +02:00
Yorick Peterse aa3fbcf522 Revamp descendant compiler specs 2015-08-28 15:29:09 +02:00
Yorick Peterse 70bea2071c Fixed ancestor-or-self relative to attributes
Per libxml behaviour this axis shouldn't match attributes when using
"ancestor-or-self::*".
2015-08-27 10:49:32 +02:00
Yorick Peterse d5aad9c1c9 Revamp descendant-or-self compiler specs 2015-08-27 10:34:25 +02:00
Yorick Peterse 8f341b40d6 Revamp child axis compiler specs 2015-08-27 09:30:53 +02:00
Yorick Peterse ed31b9f1d3 Revamp compiler specs for the attribute axis 2015-08-26 22:51:04 +02:00
Yorick Peterse 4fb7e2f6ce Revamped ancestor/ancestor-or-self axis specs
This makes it easier to get more natural spec descriptions without
having to write them entirely by hand.
2015-08-26 22:35:13 +02:00
Yorick Peterse 9899a419b7 Added Attribute#each_ancestor 2015-08-26 22:26:46 +02:00
Yorick Peterse 365a9e9fa9 Replace (path) nodes with nested nodes
This changes the XPath AST so that every segment in a path (e.g.
foo/bar) is parsed as a child node of the node that precedes it. For
example, take the following expression:

    foo/bar

This used to be parsed into the following AST:

    (path
      (axis "child" (test nil "foo"))
      (axis "child" (test nil "bar")))

This is now parsed into the following AST:

    (axis "child"
      (test nil "foo")
      (axis "child"
        (test nil "bar")))

This new AST is much easier to deal with in the XPath::Compiler class,
especially when trying to ensure that each segment operates on the
correct input.

This commit also fixes parsing of type tests with predicates, such as:

    comment()[10]

This used to throw a parser error.
2015-08-26 10:16:48 +02:00
Yorick Peterse 37ac844ebe Fixed spec for sum() in a predicate
The return value of sum() should be used as a NodeSet index, not merely
just a truthy value.
2015-08-20 00:54:06 +02:00
Yorick Peterse 94ec1b5f5d Corrected the last() compiler spec
This spec didn't match the correct nodes due to `1 < last()` always
evaluating to true in this particular case.
2015-08-20 00:23:58 +02:00
Yorick Peterse 7a4cb76d39 Cleaned up XPath last() specs a bit 2015-08-19 23:05:25 +02:00
Yorick Peterse f41f6ff0c8 Only wrap followed_by nodes in begin/end 2015-08-19 20:14:24 +02:00
Yorick Peterse a30cdba8d0 Fixed XPath compiler support for not() 2015-08-19 20:14:24 +02:00
Yorick Peterse 8e60c69def Added spec for not() in a predicate 2015-08-19 20:14:24 +02:00
Yorick Peterse 0dcee637d3 Added Ruby::Node#if_false 2015-08-19 20:14:24 +02:00
Yorick Peterse 4b50a161ed Wrap all compiler assignments in begin/end
This is much safer than having to explicitly call "wrap" in a potential
large amount of places.
2015-08-19 20:14:24 +02:00