diff --git a/.github/workflows/rake.yml b/.github/workflows/rake.yml new file mode 100644 index 0000000..4ce1372 --- /dev/null +++ b/.github/workflows/rake.yml @@ -0,0 +1,44 @@ +# Reusable workflow from metanorma/ci. The `before-setup-ruby` hook +# installs libtaurus v0.12.0 from source on each runner and exports +# TAURUS_LIB_PATH so the FFI binding finds the shared library. +name: rake + +permissions: + contents: write + packages: write + +on: + push: + branches: [ master, main ] + tags: [ v* ] + pull_request: + +jobs: + rake: + uses: metanorma/ci/.github/workflows/generic-rake.yml@main + with: + before-setup-ruby: | + set -e + mkdir -p /tmp/tb && cd /tmp/tb + curl -sL https://api.github.com/repos/lutaml/taurus/tarball/v0.12.0 | tar xz --strip-components=1 + cmake -B build -S . \ + -DCMAKE_BUILD_TYPE=Release \ + -DTAURUS_BUILD_SHARED=ON \ + -DTAURUS_BUILD_STATIC=OFF \ + -DBUILD_TESTING=OFF \ + -DTAURUS_BUILD_CLI=OFF \ + -DTAURUS_BUILD_BENCHMARKS=OFF \ + -DTAURUS_BUILD_MAN_PAGES=OFF \ + -DTAURUS_ENABLE_UTF8PROC=OFF \ + -DTAURUS_ENABLE_ICONV=OFF + cmake --build build -j 4 + LIB=$(find build/src -type f -o -type l 2>/dev/null | grep -E 'libtaurus\.(dylib|so|dll)$' | head -1) + if [ -z "$LIB" ]; then + echo "ERROR: libtaurus shared library not found after build" + find build/src -name 'libtaurus*' + exit 1 + fi + echo "TAURUS_LIB_PATH=$(pwd)/$LIB" >> "$GITHUB_ENV" + echo "Installed libtaurus at: $(pwd)/$LIB" + secrets: + pat_token: ${{ secrets.LUTAML_CI_PAT_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..833b59b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +# Auto-generated by Cimas: Do not edit it manually! +# See https://github.com/metanorma/cimas +name: release + +permissions: + contents: write + packages: write + id-token: write + +on: + workflow_dispatch: + inputs: + next_version: + description: | + Next release version. Possible values: x.y.z, major, minor, patch (or pre|rc|etc). + Also, you can pass 'skip' to skip 'git tag' and do 'gem push' for the current version + required: true + default: 'skip' + repository_dispatch: + types: [ do-release ] + +jobs: + release: + uses: relaton/support/.github/workflows/release.yml@main + with: + next_version: ${{ github.event.inputs.next_version }} + secrets: + rubygems-api-key: ${{ secrets.LUTAML_CI_RUBYGEMS_API_KEY }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 8b0f863..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Test Suite - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - test: - name: Test on ${{ matrix.os }} with Ruby ${{ matrix.ruby }} - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest] - ruby: ['3.0', '3.1', '3.2', '3.3'] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby }} - bundler-cache: true - - - name: Install dependencies - run: bundle install - - - name: Run RSpec tests - run: bundle exec rspec - - - name: Run RuboCop - run: bundle exec rubocop diff --git a/CHANGELOG.md b/CHANGELOG.md index c40c5e7..8ded087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,72 @@ All notable changes to Taurus will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.0] - 2026-08-08 + +Complete rewrite as a Nokogiri-compatible FFI binding for +[libtaurus](https://github.com/lutaml/taurus) v0.5.14. The C DOM is the +single source of truth; Ruby objects are thin FFI wrappers (one Ruby +method = one FFI call). + +### Added — XML::Document +- `XML::Document.parse(string_or_io)` and `.parse_file(path)` +- `#root`, `#free`, `#encoding`, `#name`, `#document` +- `#create_element`, `#create_text_node`, `#create_comment`, + `#create_cdata`, `#create_processing_instruction` +- `#to_xml`, `#save`, `#canonicalize` (alias `#c14n`) +- Includes `Searchable`: `#xpath`, `#at_xpath`, `#css`, `#at_css`, + `#search`, `#at` + +### Added — XML::Node hierarchy +- `Node` (base): type predicates, navigation (siblings, parent, children), + `#unlink`/`#remove`, `#line`, `#<=>`, `#traverse` +- `Element < Node`: name/content/attributes mutation, child manipulation + (`#add_child`, `#prepend_child`, `#add_next_sibling`, + `#add_previous_sibling`, `#replace`, `#swap`, `#wrap`, `#children=`) +- `Text`, `Comment`, `CDATA < Text`, `ProcessingInstruction`: + per-type content setters +- `Attr`: name/value/namespace/remove +- `Namespace`: prefix/href, derived from element's declarations +- `NodeSet`: Enumerable + Searchable + +### Added — XML::Searchable +- `#xpath`, `#at_xpath` via `taurus_xpath_eval` +- `#css`, `#at_css` via minimal CSS-to-XPath translator + (`.class`, `#id`, `[attr]`, `[attr=val]`, descendant, child, + comma-multi, `:first-child`, `:last-child`, `:only-child`, + `:empty`, `:root`, `:not(simple)`) +- `#search`, `#at` auto-detect CSS vs XPath + +### Added — XML::SAX +- `SAX::Parser#parse(string_or_io)`, `#parse_memory`, `#parse_io`, + `#parse_file` +- `SAX::Document` handler base class with Nokogiri-compatible + callback signatures + +### Added — Serialization +- `Document#to_xml`, `Element#to_xml` with indent / xml_declaration / + encoding options +- `Document#canonicalize` (whole-doc) and `Element#canonicalize` + (subtree) via `taurus_c14n_canonicalize_ex` / `_subtree_ex` +- All four C14N modes: canonical 1.0, canonical 1.1, exclusive, + with/without comments, inclusive namespace prefixes + +### Removed +- Pure-Ruby XML tree model (`lib/taurus/{document,element,node, + node_set}.rb`) — replaced by thin FFI wrappers +- Pure-Ruby XPath engine (`lib/taurus/xpath/`) — replaced by libtaurus + XPath 1.0 evaluator +- Stale bundled C source at `ext/taurus/lib/` +- `taurus` CLI (`lib/taurus/cli.rb`, `lib/taurus/commands/`) +- Pure-Ruby adapter framework (`lib/taurus/adapter*`) +- Thor runtime dependency + +### Required external dependency +- libtaurus v0.5.14 or later, installed separately. Get it from + https://github.com/lutaml/taurus/releases and place the shared + library on your system's library search path, or set + `TAURUS_LIB_PATH` to point at it. + ## [1.1.0] - 2024-12-08 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..02dd52d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,104 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project + +`taurus-ruby` is a Ruby gem that wraps the native C library `libtaurus` (built via CMake) via the `ffi` gem. It exposes an XML parser with complete XPath 1.0 support and a CLI. Target users want Nokogiri-like ergonomics with native speed. + +- Native dep: `libtaurus` shared library, built by `ext/taurus/extconf.rb` (CMake) and copied to `lib/libtaurus.{dylib,so,dll}`. +- Ruby entry point: `lib/taurus.rb` (uses `require_relative` — see "Conventions" below). +- CLI: `bin/taurus` (Thor-based), defined in `lib/taurus/cli.rb`. + +## Commands + +```bash +# Build the C library and install it into lib/ for FFI to load +bundle exec rake compile # runs ext/taurus/extconf.rb (CMake) + +# Run the full test suite (builds first) +bundle exec rake test # = spec, depends on :compile + +# Run RSpec directly (skips build dependency) +bundle exec rspec # full suite +bundle exec rspec spec/taurus/document_spec.rb # one file +bundle exec rspec spec/taurus/document_spec.rb:42 # one example by line + +# Lint +bundle exec rubocop + +# Clean build artifacts (lib/libtaurus.*, ext/taurus/build, Makefile, tmp, pkg) +bundle exec rake clean +``` + +CI (`.github/workflows/test.yml`) runs `bundle exec rspec` + `bundle exec rubocop` on Ubuntu + macOS across Ruby 3.0–3.3. + +## Architecture: current state (v1.1.0) + +``` +User Ruby code + ↓ +Taurus.parse / Taurus.parse_file (lib/taurus.rb) + ↓ FFI call (taurus_parse) — one-shot tree copy +C document → FFI::Bridge.document_from_ptr (lib/taurus/ffi/bridge.rb) + ↓ recursive hydration +Ruby Document → Element → Node → NodeSet (lib/taurus/{document,element,node,node_set}.rb) +``` + +Key directories: + +- `lib/taurus.rb` — top-level module, `parse`, `parse_file`, `xpath_evaluate`, error classes (`ParseError`, `XPathError`, `EvaluationError`). +- `lib/taurus/ffi/` — FFI plumbing: `library.rb` (bindings), `types.rb` (constants), `memory.rb` (AutoPointer wrappers), `errors.rb` (thread-local error check), `bridge.rb` (C ptr → Ruby object). +- `lib/taurus/{document,element,node,node_set}.rb` — pure-Ruby tree model (full hydration on parse). +- `lib/taurus/xpath/` — pure-Ruby XPath engine (lexer, parser, compiler, VM). XPath DOES NOT go through C currently; `lib/taurus.rb#xpath_evaluate` calls `FFI.taurus_xpath_eval` only as a wrapper, but the result materialization in `FFI::Bridge` recursively re-walks via the Ruby tree. +- `lib/taurus/adapter*` — third-party format adapters. +- `spec/taurus/` — 250+ RSpec examples covering parser, XPath, namespaces, errors, ox-compatibility. +- `ext/taurus/` — CMake-based build of `libtaurus` (sources come from the separate `lutaml/taurus` repo at build time). + +## Architecture: planned rewrite (see `TODO.impl/`) + +The five files under `TODO.impl/` describe a planned rewrite that has NOT been implemented yet. Read them before touching the Ruby/COM layer. Summary: + +1. **`01-architecture.md`** — Rewrite `taurus-ruby` as a **thin FFI wrapper** around libtaurus v0.4.2 with a **Nokogiri-compatible API**. Current code does a one-shot C→Ruby tree copy and runs XPath in Ruby; planned code keeps the C DOM as the single source of truth (Ruby objects = handles wrapping opaque pointers), so every Ruby method = one FFI call and XPath/SAX go through the C engine. + +2. **`02-ffi-declarations.md`** — Complete FFI attachment: every public function in libtaurus v0.4.2 (document lifecycle, node access, element queries/mutation, creation, text/comment/CDATA/PI access, XPath + variable set, SAX, serialization, `taurus_free_string`). Opaque typedefs: `document`, `element`, `node_ref`, `xpath_result`, `sax_parser`, `attribute`. Structs: `SAXHandler`, `SerializeOptions`. Constants for status codes, node types, XPath result types. + +3. **`03-document-node-element-nodeset.md`** — Target file layout: + ``` + lib/taurus.rb + lib/taurus/xml.rb + lib/taurus/xml/{ffi,document,node,element,text,comment,cdata, + processing_instruction,attr,node_set,searchable, + parse_options}.rb + ``` + `Node.wrap(ptr, doc)` dispatches on the C node type. Document is the only memory-owning object; Node/Element/Text/Comment/CDATA/PI/Attr are non-owning handles valid until `Document#free`. + +4. **`04-sax-parser.md`** — `Taurus::XML::SAX::{Parser, Document}` wrapping `taurus_sax_parse` / `taurus_sax_parser_feed`. FFI::Function callbacks for each event; `start_element` walks the NULL-terminated `const char**` attribute array. Streaming via incremental `feed`. + +5. **`05-serialize-c14n-memory-specs-css.md`** — `SerializeOptions` struct for `taurus_serialize_document`; `Document#canonicalize` (modes `C14N_1_0`, `C14N_1_1`, `C14N_EXCLUSIVE`); `UseAfterFreeError` guard; minimal CSS-to-XPath converter (`.class`, `#id`, `[attr]`, `[attr=val]`, `:first-child`, `:last-child`); spec layout under `spec/xml/{parse,document,node,element,node_set,xpath,sax,serialize,c14n,memory}_spec.rb`. + +### Memory ownership rules (planned) + +| Ruby class | Owns C memory? | Free function | +|---|---|---| +| `Document` | YES | `taurus_document_free` | +| `Node`/`Element`/text/CDATA/PI/Attr | NO (borrowed) | freed transitively by Document | +| `NodeSet` (XPath result) | YES | `taurus_xpath_result_free` | +| SAX handler closures | callback lifetime | `taurus_sax_parser_free` | + +GC safety net: `ObjectSpace.define_finalizer` capturing the **raw pointer value**, not the Ruby wrapper. Finalizers must not double-free after explicit `#free`. + +## Reference material + +- Nokogiri source (`~/src/external/nokogiri/`) — `lib/nokogiri/xml/{node,node_set,document,searchable}.rb` are the API shape targets. +- libtaurus public headers (`src/include/taurus/{types,taurus}.h`, `src/include/taurus/{dom,xpath,sax}/*.h`) — the single source of truth for FFI declarations. Target tag: `v0.4.2`. +- `docs/FFI_ARCHITECTURE.md` — describes the v0.5.0 FFI design (AutoPointer, two-pointer strategy for XPath). The planned rewrite supersedes some of this (no recursive hydration, no two-pointer — the Document pointer alone suffices because Node objects stay as C handles). +- `docs/BUILD.md` — CMake build reference for libtaurus itself. + +## Conventions (project-specific) + +- **Autoload, not require_relative.** TODO 3 is explicit: `lib/taurus.rb` → `autoload :XML, 'taurus/xml'`; `lib/taurus/xml.rb` → `autoload :Document, 'taurus/xml/document'`. Autoload entries live in the **immediate parent namespace's file** (create that file if missing). The current `lib/taurus.rb` uses `require_relative` — when implementing TODO 3, do not retrofit require_relative into the new layout. +- **No `instance_variable_set`/`_get` cross-object.** TODO 3 is explicit. The current `lib/taurus.rb` and `lib/taurus/ffi/bridge.rb` use `instance_variable_get(:@_c_ptr)` heavily — that pattern is debt to migrate, not a model to copy. In the rewrite, expose `c_ptr`/`document` as public `attr_reader`s and access via those. +- **No `respond_to?` type checks.** Use `is_a?`. The current `lib/taurus.rb#xpath_evaluate` checks `context_node != doc` to disambiguate — fine. Don't add `respond_to?(:c_ptr)` style checks. +- **No doubles in specs.** The existing `spec/taurus/` specs use real model instances (XML strings → `Taurus.parse` → real `Document`/`Element`/`NodeSet`). Keep it that way. +- **Forward compatibility:** Keep `Taurus.parse` / `Taurus.parse_file` as the existing top-level API during the rewrite. The new `Taurus::XML.parse` may coexist. \ No newline at end of file diff --git a/Gemfile b/Gemfile index 7fde74a..ca27aa7 100644 --- a/Gemfile +++ b/Gemfile @@ -2,23 +2,13 @@ source "https://rubygems.org" -# Specify your gem's dependencies in taurus.gemspec gemspec -# FFI for Ruby-C bindings -gem "ffi", "~> 1.15" - -gem "benchmark-ips" -gem "memory_profiler" -gem "moxml", "~> 0.1" -gem "nokogiri", "~> 1.18" -gem "oga", "~> 3.4" -gem "ox", "~> 2.14" -gem "asciidoctor" -gem "rake-compiler" gem "rake" gem "rspec" gem "rubocop" -gem "ruby-prof" -gem "thor" -gem "yard" \ No newline at end of file + +group :benchmark do + gem "benchmark-ips" + gem "nokogiri" +end diff --git a/Gemfile.lock b/Gemfile.lock index d385b7b..adfde2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,34 +3,36 @@ PATH specs: taurus (0.1.0) ffi (~> 1.15) - thor (~> 1.0) GEM remote: https://rubygems.org/ specs: - ansi (1.5.0) - asciidoctor (2.0.26) ast (2.4.3) - base64 (0.3.0) - benchmark-ips (2.14.0) - bigdecimal (3.3.1) + benchmark-ips (2.15.1) diff-lcs (1.6.2) + ffi (1.17.2) + ffi (1.17.2-aarch64-linux-gnu) ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x64-mingw-ucrt) ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) json (2.17.1) language_server-protocol (3.17.0.5) lint_roller (1.1.0) - memory_profiler (1.1.0) - moxml (0.1.10) - nokogiri (1.18.10-arm64-darwin) + mini_portile2 (2.8.9) + nokogiri (1.19.4) + mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.18.10-x86_64-darwin) + nokogiri (1.19.4-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x64-mingw-ucrt) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-gnu) racc (~> 1.4) - oga (3.4) - ast - ruby-ll (~> 2.1) - ox (2.14.23) - bigdecimal (>= 3.0) parallel (1.27.0) parser (3.3.10.0) ast (~> 2.4.1) @@ -39,8 +41,6 @@ GEM racc (1.8.1) rainbow (3.1.1) rake (13.3.1) - rake-compiler (1.3.0) - rake regexp_parser (2.11.3) rspec (3.13.2) rspec-core (~> 3.13.0) @@ -69,39 +69,28 @@ GEM rubocop-ast (1.48.0) parser (>= 3.3.7.2) prism (~> 1.4) - ruby-ll (2.1.4) - ansi - ast - ruby-prof (1.7.2) - base64 ruby-progressbar (1.13.0) - thor (1.4.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) - yard (0.9.38) PLATFORMS + aarch64-linux arm64-darwin + x64-mingw-ucrt + x64-mingw32 x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-mswin32 DEPENDENCIES - asciidoctor benchmark-ips - ffi (~> 1.15) - memory_profiler - moxml (~> 0.1) - nokogiri (~> 1.18) - oga (~> 3.4) - ox (~> 2.14) + nokogiri rake - rake-compiler rspec rubocop - ruby-prof taurus! - thor - yard BUNDLED WITH 2.6.9 diff --git a/Rakefile b/Rakefile index 041053c..209fdbd 100644 --- a/Rakefile +++ b/Rakefile @@ -2,38 +2,6 @@ require "bundler/gem_tasks" require "rspec/core/rake_task" -require "fileutils" -# Build task for libtaurus FFI library -desc "Compile libtaurus library" -task :compile do - ext_dir = File.expand_path('ext/taurus', __dir__) - Dir.chdir(ext_dir) do - ruby 'extconf.rb' - end -end - -# Ruby tests RSpec::Core::RakeTask.new(:spec) -task spec: :compile - -# Clean build artifacts -desc "Clean build artifacts" -task :clean do - FileUtils.rm_f(Dir.glob("lib/libtaurus.{so,bundle,dll,dylib}")) - FileUtils.rm_rf("ext/taurus/build") - FileUtils.rm_f("ext/taurus/Makefile") - FileUtils.rm_rf(["tmp", "pkg"]) - puts "Cleaned build artifacts" -end - -# All tests -desc "Run all tests" -task test: :spec do - puts "\n" + "=" * 80 - puts "TEST SUITE COMPLETE" - puts "=" * 80 -end - -# Default task -task default: :test \ No newline at end of file +task default: :spec diff --git a/TODO.impl/01-architecture.md b/TODO.impl/01-architecture.md new file mode 100644 index 0000000..596adba --- /dev/null +++ b/TODO.impl/01-architecture.md @@ -0,0 +1,217 @@ +# TODO 1 — Architecture: C-backed Nokogiri-compatible Ruby binding + +## Goal + +Rewrite `taurus-ruby` to be a **thin FFI wrapper** around libtaurus v0.4.2, +exposing a **Nokogiri-compatible API**. The C DOM is the single source of +truth — no Ruby-side tree copy. + +## Current state (problem) + +The existing `taurus-ruby` has: +- A **pure-Ruby** XML tree model (Document < Element, Node, NodeSet) +- A FFI bridge that does a **one-shot copy** from C to Ruby on parse +- A **pure-Ruby XPath engine** (lexer, parser, compiler, VM in Ruby) + +This defeats libtaurus's performance: the C library's optimized bytecode +VM + element index are never used. XPath goes through the slow Ruby engine. + +## Target architecture + +``` +User Ruby code + ↓ +Taurus::XML::Document / Node / NodeSet (thin Ruby wrappers) + ↓ FFI +libtaurus v0.4.2 (C99: DOM, XPath bytecode VM, element index) +``` + +Key principles: +- **Every Ruby method is a single FFI call** to the C library. +- **No Ruby-side tree copy.** The C DOM is the truth; Ruby objects are + handles (wrapping opaque pointers). +- **XPath goes through C.** `doc.xpath('//book')` calls + `taurus_xpath_eval` directly. No Ruby XPath engine. +- **SAX goes through C.** `Taurus::XML::SAX::Parser` wraps + `taurus_sax_parse` with Ruby callback dispatch. + +## Module structure (Nokogiri-compatible) + +```ruby +module Taurus + module XML + # Top-level parse entry points + def self.parse(string_or_io) → Document + def self.parse_options → ParseOptions + + class Document < Node + def root → Element (or nil) + def create_element(name) → Element + def create_text_node(text) → Text + def to_xml(options) → String + def xpath(expr) → NodeSet | Float | String | Boolean + def at_xpath(expr) → Node (or nil) + def search(expr) → NodeSet + def canonicalize(...) → String + def free → void (explicit) + end + + class Node + include Searchable + + def name → String + def content / text → String + def [](attr_name) → String (or nil) + def []=(attr_name, value) + def attributes → Hash {String => Attr} + def children → NodeSet + def child → Node (or nil) + def first_element_child → Element (or nil) + def last_element_child → Element (or nil) + def next_sibling → Node (or nil) + def previous_sibling → Node (or nil) + def parent → Node (or nil) + def document → Document + def type → Integer (element/text/comment/cdata/pi) + def element? → Boolean + def text? → Boolean + def comment? → Boolean + def cdata? → Boolean + def processing_instruction? → Boolean + def add_child(node) → Node + def add_next_sibling(node) → Node + def add_previous_sibling(node) → Node + def remove → Node + def replace(node) → Node + def to_xml(options) → String + def inner_html → String + def traverse(&block) + end + + class Element < Node + def add_class(name) + def remove_class(name) + def classes → Array + end + + class Text < Node; end + class Comment < Node; end + class CDATA < Node; end + class ProcessingInstruction < Node; end + class Attr + def name → String + def value → String + def value=(val) + def parent → Element + def remove + end + + class NodeSet + include Enumerable + include Searchable + + def length / size → Integer + def first(n) → Node | NodeSet + def last → Node + def [](index) → Node + def each(&block) + def empty? → Boolean + def xpath(expr) → NodeSet + def search(expr) → NodeSet + def to_xml → String + def inner_text → String + end + + module Searchable + def xpath(*paths) → NodeSet | Float | String | Boolean + def at_xpath(*paths) → Node (or nil) + def css(*selectors) → NodeSet (converts CSS to XPath) + def at_css(*selectors) → Node (or nil) + def search(*args) → NodeSet (auto-detect CSS/XPath) + def at(*args) → Node (or nil) + end + + class ParseOptions + DEFAULT_XML = ... + RECOVER = ... + NOERROR = ... + NOWARNING = ... + NOCDATA = ... + STRICT = ... + end + + module SAX + class Parser + def initialize(handler = DocHandler.new) + def parse(io_or_string) + end + + class Document + def start_element(name, attrs = []) + def end_element(name) + def characters(string) + def start_document + def end_document + def comment(string) + def cdata(string) + def processing_instruction(name, content) + def error(message, line, column) + end + end + end +end +``` + +## Memory model + +- **Document** owns the C document pool. `Taurus::XML::Document.new` + calls `taurus_parse_string` → returns a `TaurusDocument` pointer. + `Document#free` calls `taurus_document_free`. Auto-free via + `ObjectSpace.define_finalizer` as a safety net (but callers should + call `#free` explicitly for predictable lifecycle). +- **Node / Element / Text etc.** are **non-owning handles** wrapping + a C pointer. The pointer is valid as long as the parent Document + is alive. Freeing a Node just drops the Ruby wrapper; the C node + lives until `Document#free`. +- **NodeSet** wraps a `TaurusXPathResult` pointer from + `taurus_xpath_eval`. Freeing a NodeSet calls + `taurus_xpath_result_free`. +- **Attr** wraps a C attribute pointer (owned by the parent element's + pool). Non-owning. + +## CSS support + +Nokogiri supports CSS selectors via `css()` and `at_css()`. Taurus +doesn't have a CSS engine in C, so CSS-to-XPath conversion must be +done in Ruby. Options: +1. Use the `css_parser` gem (depends on `racc`). +2. Write a minimal CSS-to-XPath converter in pure Ruby. + +For v0.4.2 compatibility, option 2 (minimal converter) is recommended. +Nokogiri's CSS selector support is comprehensive but the common subset +is small: `tag`, `.class`, `#id`, `> child`, `descendant`, +`[attr]`, `[attr=value]`, `:first-child`, `:last-child`, `:not(...)`. + +## Dependencies + +```ruby +# taurus.gemspec +spec.add_dependency 'ffi', '~> 1.16' +``` + +No other runtime dependencies. No C extension compilation needed — +just FFI to the pre-built libtaurus shared library. + +## Reference material + +- Nokogiri source: `~/src/external/nokogiri/` + - `lib/nokogiri/xml/node.rb` — 77 public methods + - `lib/nokogiri/xml/node_set.rb` — 31 public methods + - `lib/nokogiri/xml/document.rb` — 22 public methods + - `lib/nokogiri/xml/searchable.rb` — xpath/css/search module +- libtaurus public headers: `src/include/taurus/` + - `types.h` — opaque handle typedefs + - `dom/document.h`, `dom/element.h`, `dom/serialize.h` + - `xpath/xpath.h` — XPath eval API + - `sax/sax.h` — SAX parser API +- libtaurus v0.4.2: tag `v0.4.2` on `github.com:lutaml/taurus` diff --git a/TODO.impl/02-ffi-declarations.md b/TODO.impl/02-ffi-declarations.md new file mode 100644 index 0000000..125c92b --- /dev/null +++ b/TODO.impl/02-ffi-declarations.md @@ -0,0 +1,236 @@ +# TODO 2 — FFI declarations: complete libtaurus v0.4.2 public API + +## Goal + +Create `lib/taurus/xml/ffi.rb` that attaches to EVERY public function +in libtaurus v0.4.2 via the `ffi` gem. This is the single source of +truth for the C ↔ Ruby boundary. + +## Library loading + +```ruby +module Taurus + module XML + module FFI + extend ::FFI + + ffi_lib [ + ENV['TAURUS_LIB_PATH'], + 'taurus', + '/usr/local/lib/libtaurus.dylib', + '/usr/local/lib/libtaurus.so', + File.expand_path('../../../build/src/libtaurus.dylib', __dir__), + File.expand_path('../../../build/src/libtaurus.so', __dir__), + ].compact + end + end +end +``` + +## Opaque type declarations + +```ruby +typedef :pointer, :document +typedef :pointer, :element +typedef :pointer, :node_ref +typedef :pointer, :xpath_result +typedef :pointer, :sax_parser +typedef :pointer, :attribute +``` + +## Complete function list (attach all of these) + +Source: `src/include/taurus/types.h`, `src/include/taurus.h`, and +`src/include/taurus/*.h`. + +### Version +```ruby +attach_function :taurus_version, [], :string +``` + +### Document lifecycle +```ruby +attach_function :taurus_parse_string, [:string, :size_t, :pointer], :document +attach_function :taurus_document_free, [:document], :void +attach_function :taurus_document_root, [:document], :element +attach_function :taurus_document_serialize, [:document, :pointer], :pointer +attach_function :taurus_document_set_strict, [:document, :int], :void +attach_function :taurus_xinclude_process, [:document, :string], :int +``` + +### Node access +```ruby +attach_function :taurus_node_get_type, [:node_ref], :int +attach_function :taurus_node_first_child, [:node_ref], :node_ref +attach_function :taurus_node_next_sibling, [:node_ref], :node_ref +attach_function :taurus_node_previous_sibling, [:node_ref], :node_ref +attach_function :taurus_node_child_count, [:node_ref], :size_t +attach_function :taurus_node_as_element, [:node_ref], :element +attach_function :taurus_element_as_node, [:element], :node_ref +attach_function :taurus_element_first_child_any, [:element], :element +``` + +### Element queries +```ruby +attach_function :taurus_element_name, [:element], :string +attach_function :taurus_element_text, [:element], :string +attach_function :taurus_element_attribute, [:element, :string, :string], :string +attach_function :taurus_element_attribute_count, [:element], :size_t +attach_function :taurus_element_first_attribute, [:element], :pointer +attach_function :taurus_element_parent, [:element], :element +attach_function :taurus_element_next_sibling_any, [:element], :element +attach_function :taurus_element_get_namespace_uri, [:element], :string +attach_function :taurus_element_get_prefix, [:element], :string +attach_function :taurus_element_get_name, [:element], :string +``` + +### Element mutation +```ruby +attach_function :taurus_element_set_name, [:element, :string], :void +attach_function :taurus_element_set_attribute, [:element, :string, :string], :void +attach_function :taurus_element_remove_attribute, [:element, :string], :void +attach_function :taurus_element_append_child, [:element, :element], :int +attach_function :taurus_element_create_child, [:element, :string], :element +attach_function :taurus_element_set_text, [:element, :string], :void +attach_function :taurus_element_remove_child, [:element, :element], :void +``` + +### Element creation +```ruby +attach_function :taurus_element_create, [:string], :element +attach_function :taurus_text_node_create, [:string], :element +attach_function :taurus_comment_node_create, [:string], :element +attach_function :taurus_cdata_node_create, [:string], :element +attach_function :taurus_pi_node_create, [:string, :string], :element +``` + +### Text / Comment / CDATA / PI access +```ruby +attach_function :taurus_text_node_get_content, [:node_ref], :string +attach_function :taurus_comment_node_get_content, [:node_ref], :string +attach_function :taurus_cdata_node_get_content, [:node_ref], :string +attach_function :taurus_pi_node_get_target, [:node_ref], :string +attach_function :taurus_pi_node_get_data, [:node_ref], :string +``` + +### XPath +```ruby +attach_function :taurus_xpath_eval, + [:document, :element, :string], :xpath_result +attach_function :taurus_xpath_eval_with_vars, + [:document, :string, :pointer], :xpath_result +attach_function :taurus_xpath_result_type, [:xpath_result], :int +attach_function :taurus_xpath_result_count, [:xpath_result], :size_t +attach_function :taurus_xpath_result_get, [:xpath_result, :size_t], :element +attach_function :taurus_xpath_result_boolean, [:xpath_result], :int +attach_function :taurus_xpath_result_number, [:xpath_result], :double +attach_function :taurus_xpath_result_string, [:xpath_result], :pointer +attach_function :taurus_xpath_result_free, [:xpath_result], :void +``` + +### XPath variables +```ruby +attach_function :taurus_xpath_variable_set_new, [], :pointer +attach_function :taurus_xpath_variable_set_free, [:pointer], :void +attach_function :taurus_xpath_variable_set_boolean, [:pointer, :string, :int], :int +attach_function :taurus_xpath_variable_set_number, [:pointer, :string, :double], :int +attach_function :taurus_xpath_variable_set_string, [:pointer, :string, :string], :int +``` + +### SAX +```ruby +# TaurusSAXHandler is a struct of function pointers. Use FFI::Struct. +class SAXHandler < ::FFI::Struct + layout \ + :start_document, :pointer, + :end_document, :pointer, + :start_element, :pointer, + :end_element, :pointer, + :characters, :pointer, + :comment, :pointer, + :cdata, :pointer, + :processing_instruction, :pointer, + :start_prefix_mapping, :pointer, + :end_prefix_mapping, :pointer, + :error, :pointer +end + +attach_function :taurus_sax_parse, + [:string, :size_t, SAXHandler.by_pointer, :pointer], :int +attach_function :taurus_sax_parser_create, + [SAXHandler.by_pointer, :pointer], :sax_parser +attach_function :taurus_sax_parser_feed, + [:sax_parser, :string, :size_t, :int], :int +attach_function :taurus_sax_parser_free, [:sax_parser], :void +attach_function :taurus_sax_parser_set_streaming, + [:sax_parser, :int], :int +``` + +### Serialization +```ruby +# TaurusSerializeOptions struct +class SerializeOptions < ::FFI::Struct + layout \ + :indent, :int, + :xml_declaration, :int, + :no_empty_tags, :int, + :preserve_whitespace, :int +end + +attach_function :taurus_serialize_document, + [:document, :pointer], :pointer +attach_function :taurus_c14n_canonicalize, + [:document, :int, :int], :pointer +``` + +### Memory +```ruby +attach_function :taurus_free_string, [:pointer], :void +``` + +## Status codes + +```ruby +TAURUS_OK = 0 +TAURUS_ERROR_MEMORY = -1 +TAURUS_ERROR_PARSE = -2 +TAURUS_ERROR_XPATH = -3 +TAURUS_ERROR_NULL_ARG = -4 +TAURUS_ERROR_INVALID_ARG = -5 +TAURUS_ERROR_NOT_FOUND = -6 +TAURUS_ERROR_IO = -7 +``` + +## Node type constants + +```ruby +NODE_ELEMENT = 0 +NODE_ATTRIBUTE = 1 +NODE_TEXT = 2 +NODE_COMMENT = 3 +NODE_CDATA = 4 +NODE_PI = 5 +NODE_DOCTYPE = 6 +``` + +## XPath result type constants + +```ruby +XPATH_NODESET = 0 +XPATH_BOOLEAN = 1 +XPATH_NUMBER = 2 +XPATH_STRING = 3 +``` + +## Notes + +- Use `Blocking: true` for SAX callbacks (FFI::Function). +- Use `AutoPointer` for document and xpath_result to get automatic + cleanup. But ALSO provide explicit `#free` methods since GC timing + is non-deterministic. +- The `taurus_element_*` functions that return `:string` return + document-owned strings (valid until `taurus_document_free`). + Ruby copies them automatically on FFI return — safe. +- Functions that return `:pointer` for strings (like + `taurus_xpath_result_string`, `taurus_serialize_document`) return + heap-owned strings that the caller must free via `taurus_free_string`. diff --git a/TODO.impl/03-document-node-element-nodeset.md b/TODO.impl/03-document-node-element-nodeset.md new file mode 100644 index 0000000..8b6fd36 --- /dev/null +++ b/TODO.impl/03-document-node-element-nodeset.md @@ -0,0 +1,382 @@ +# TODO 3 — Document, Node, Element, NodeSet, Searchable implementation + +## Goal + +Implement the core Nokogiri-compatible Ruby classes backed by FFI +calls to libtaurus. Each Ruby method = one C function call. + +## File layout + +``` +lib/taurus.rb # autoload + version +lib/taurus/xml.rb # XML module + parse entry points +lib/taurus/xml/ffi.rb # FFI declarations (TODO 2) +lib/taurus/xml/document.rb # Document class +lib/taurus/xml/node.rb # Node base class +lib/taurus/xml/element.rb # Element < Node +lib/taurus/xml/text.rb # Text < Node +lib/taurus/xml/comment.rb # Comment < Node +lib/taurus/xml/cdata.rb # CDATA < Node +lib/taurus/xml/processing_instruction.rb # ProcessingInstruction < Node +lib/taurus/xml/attr.rb # Attr class +lib/taurus/xml/node_set.rb # NodeSet class +lib/taurus/xml/searchable.rb # Searchable mixin (xpath/css/search) +lib/taurus/xml/parse_options.rb # ParseOptions class +``` + +## Autoload pattern + +```ruby +# lib/taurus.rb +module Taurus + autoload :XML, 'taurus/xml' +end + +# lib/taurus/xml.rb +module Taurus + module XML + autoload :FFI, 'taurus/xml/ffi' + autoload :Document, 'taurus/xml/document' + autoload :Node, 'taurus/xml/node' + autoload :Element, 'taurus/xml/element' + autoload :Text, 'taurus/xml/text' + autoload :Comment, 'taurus/xml/comment' + autoload :CDATA, 'taurus/xml/cdata' + autoload :ProcessingInstruction, 'taurus/xml/processing_instruction' + autoload :Attr, 'taurus/xml/attr' + autoload :NodeSet, 'taurus/xml/node_set' + autoload :Searchable, 'taurus/xml/searchable' + autoload :ParseOptions, 'taurus/xml/parse_options' + + def self.parse(string_or_io, options = nil) + xml = string_or_io.respond_to?(:read) ? string_or_io.read : string_or_io + status = ::FFI::MemoryPointer.new(:int) + ptr = FFI.taurus_parse_string(xml, xml.bytesize, status) + raise ParseError, "taurus_parse_string failed (status=#{status.read_int})" if ptr.nil? || ptr.null? + Document.wrap(ptr) + end + end +end +``` + +## Document + +```ruby +class Taurus::XML::Document < Taurus::XML::Node + def self.wrap(c_ptr) + doc = allocate + doc.instance_variable_set(:@c_ptr, c_ptr) + doc + end + + def root + ptr = FFI.taurus_document_root(@c_ptr) + return nil if ptr.nil? || ptr.null? + Taurus::XML::Element.wrap(ptr, self) + end + + def xpath(expr) + Taurus::XML::XPath.evaluate(@c_ptr, nil, expr) + end + + def at_xpath(expr) + result = xpath(expr) + result.is_a?(NodeSet) ? result.first : result + end + + def to_xml(options = {}) + opts = SerializeOptions.new + opts[:indent] = options[:indent] || 0 + opts[:xml_declaration] = options[:no_decl] ? 0 : 1 + ptr = FFI.taurus_serialize_document(@c_ptr, opts.pointer) + return '' if ptr.nil? || ptr.null? + str = ptr.read_string + FFI.taurus_free_string(ptr) + str + end + + def free + return unless @c_ptr + FFI.taurus_document_free(@c_ptr) + @c_ptr = nil + end +end +``` + +## Node (base class) + +```ruby +class Taurus::XML::Node + attr_reader :c_ptr, :document + + def initialize(c_ptr, document) + @c_ptr = c_ptr + @document = document + end + + def self.wrap(c_ptr, document) + type = FFI.taurus_node_get_type(c_ptr) + case type + when NODE_ELEMENT then Taurus::XML::Element.new(c_ptr, document) + when NODE_TEXT then Taurus::XML::Text.new(c_ptr, document) + when NODE_COMMENT then Taurus::XML::Comment.new(c_ptr, document) + when NODE_CDATA then Taurus::XML::CDATA.new(c_ptr, document) + when NODE_PI then Taurus::XML::ProcessingInstruction.new(c_ptr, document) + else new(c_ptr, document) + end + end + + def name + raise NotImplementedError + end + + def content + raise NotImplementedError + end + + def type + FFI.taurus_node_get_type(@c_ptr) + end + + def element?; type == NODE_ELEMENT; end + def text?; type == NODE_TEXT; end + def comment?; type == NODE_COMMENT; end + def cdata?; type == NODE_CDATA; end + def processing_instruction?; type == NODE_PI; end + + def next_sibling + ptr = FFI.taurus_node_next_sibling(@c_ptr) + return nil if ptr.null? + Node.wrap(ptr, @document) + end + + def previous_sibling + ptr = FFI.taurus_node_previous_sibling(@c_ptr) + return nil if ptr.null? + Node.wrap(ptr, @document) + end + + def parent + ptr = FFI.taurus_element_parent(@c_ptr) # only works for elements + return nil if ptr.null? + Element.wrap(ptr, @document) + end + + def children + NodeSet.new(@document, self) + end + + def child + ptr = FFI.taurus_node_first_child(@c_ptr) + return nil if ptr.null? + Node.wrap(ptr, @document) + end + + def traverse(&block) + return enum_for(:traverse) unless block_given? + block.call(self) + children.each { |c| c.traverse(&block) } + end + + include Searchable +end +``` + +## Element + +```ruby +class Taurus::XML::Element < Taurus::XML::Node + def name + FFI.taurus_element_name(@c_ptr) + end + + def name=(n) + FFI.taurus_element_set_name(@c_ptr, n) + end + + def content + FFI.taurus_element_text(@c_ptr) + end + alias_method :text, :content + + def [](attr_name) + FFI.taurus_element_attribute(@c_ptr, attr_name, nil) + end + + def []=(attr_name, value) + FFI.taurus_element_set_attribute(@c_ptr, attr_name, value.to_s) + end + + def attributes + # Walk the C attribute list, build a hash of Attr objects + result = {} + count = FFI.taurus_element_attribute_count(@c_ptr) + # ... iterate attribute linked list ... + result + end + + def keys + attributes.keys + end + + def values + attributes.values.map(&:value) + end + + def first_element_child + ptr = FFI.taurus_element_first_child_any(@c_ptr) + return nil if ptr.null? + Element.wrap(ptr, @document) + end + + def add_child(node) + FFI.taurus_element_append_child(@c_ptr, node.c_ptr) + node + end + + def add_class(names) ... end + def remove_class(names = nil) ... end + def classes + (self['class'] || '').split + end +end +``` + +## NodeSet + +```ruby +class Taurus::XML::NodeSet + include Enumerable + include Searchable + + def initialize(document, result_ptr = nil) + @document = document + @result_ptr = result_ptr # TaurusXPathResult pointer + end + + def length + return @cached_length if @cached_length + @cached_length = + @result_ptr ? FFI.taurus_xpath_result_count(@result_ptr) : 0 + end + alias_method :size, :length + + def [](index) + return nil if index < 0 || index >= length + ptr = FFI.taurus_xpath_result_get(@result_ptr, index) + return nil if ptr.null? + Node.wrap(ptr, @document) + end + + def first(n = nil) + return self[0] if n.nil? + NodeSet.new(@document).tap { |ns| n.times { |i| ns << self[i] } } + end + + def last + self[length - 1] + end + + def each + return enum_for(:each) unless block_given? + length.times { |i| yield self[i] } + end + + def empty? + length == 0 + end + + def inner_text + map(&:content).join + end + + def to_xml + map(&:to_xml).join + end + + def free + return unless @result_ptr + FFI.taurus_xpath_result_free(@result_ptr) + @result_ptr = nil + end +end +``` + +## Searchable (mixin for xpath/css) + +```ruby +module Taurus::XML::Searchable + def xpath(*paths) + expr = paths.join(' | ') + result_ptr = FFI.taurus_xpath_eval( + document.c_ptr, + respond_to?(:c_ptr) ? c_ptr : nil, + expr + ) + return nil if result_ptr.null? + + result_type = FFI.taurus_xpath_result_type(result_ptr) + case result_type + when XPATH_NODESET + NodeSet.new(@document, result_ptr) + when XPATH_NUMBER + n = FFI.taurus_xpath_result_number(result_ptr) + FFI.taurus_xpath_result_free(result_ptr) + n + when XPATH_STRING + s = FFI.taurus_xpath_result_string(result_ptr) + FFI.taurus_xpath_result_free(result_ptr) + s + when XPATH_BOOLEAN + b = FFI.taurus_xpath_result_boolean(result_ptr) + FFI.taurus_xpath_result_free(result_ptr) + b == 1 + end + end + + def at_xpath(*paths) + ns = xpath(*paths) + ns.is_a?(NodeSet) ? ns.first : ns + end + + def search(*args) + expr = args.first.to_s + if expr.start_with?('/') || expr.start_with?('//') + xpath(expr) + else + css(expr) + end + end + + def at(*args) + result = search(*args) + result.is_a?(NodeSet) ? result.first : result + end + + def css(*selectors) + # Convert CSS to XPath (minimal converter) + xpath_expr = CssToXPath.convert(selectors.join(', ')) + xpath(xpath_expr) + end + + def at_css(*selectors) + result = css(*selectors) + result.is_a?(NodeSet) ? result.first : result + end +end +``` + +## Implementation notes + +- **No `require_relative`** anywhere. Use `autoload` defined in the + immediate parent namespace's file (e.g., `lib/taurus/xml.rb`). +- **No `instance_variable_set`/`instance_variable_get`** on other + objects. Use public accessor methods. +- **No `send`** to call private methods. +- **No `respond_to?`** for type checks. Use `is_a?`. +- Wrap C pointers in Ruby objects via `Node.wrap(ptr, doc)` which + dispatches on the C node type. +- Node objects are lightweight: just a pointer + document reference. + No caching of properties (each call goes through FFI). +- The document is the ONLY object that owns memory. All other objects + are non-owning handles. diff --git a/TODO.impl/04-sax-parser.md b/TODO.impl/04-sax-parser.md new file mode 100644 index 0000000..5ce2bdd --- /dev/null +++ b/TODO.impl/04-sax-parser.md @@ -0,0 +1,203 @@ +# TODO 4 — SAX parser wrapper + +## Goal + +Wrap libtaurus's C SAX parser (`taurus_sax_parse`, `taurus_sax_parser_feed`) +behind a Nokogiri-compatible Ruby SAX API. + +## Nokogiri SAX API (target) + +```ruby +class MyHandler < Nokogiri::XML::SAX::Document + def start_document; end + def end_document; end + def start_element(name, attrs = []); end + def end_element(name); end + def characters(string); end + def comment(string); end + def cdata_block(string); end + def processing_instruction(name, content); end + def warning(msg); end + def error(msg); end +end + +parser = Nokogiri::XML::SAX::Parser.new(MyHandler.new) +parser.parse(File.read('file.xml')) +# or streaming: +parser.parse(io) # reads in chunks +``` + +## Taurus SAX API (source) + +From `src/include/taurus/sax/sax.h`: + +```c +struct TaurusSAXHandler { + void (*start_document)(void* user_data); + void (*end_document)(void* user_data); + void (*start_element)(void* user_data, const char* name, const char** attrs); + void (*end_element)(void* user_data, const char* name); + void (*characters)(void* user_data, const char* text, size_t len); + void (*comment)(void* user_data, const char* comment); + void (*cdata)(void* user_data, const char* cdata); + void (*processing_instruction)(void* user_data, const char* target, const char* data); + void (*start_prefix_mapping)(void* user_data, const char* prefix, const char* uri); + void (*end_prefix_mapping)(void* user_data, const char* prefix); + void (*error)(void* user_data, const char* message, int line, int column); +}; + +int taurus_sax_parse(const char* xml, size_t len, + TaurusSAXHandler* handler, void* user_data); +TaurusSAXParser* taurus_sax_parser_create(TaurusSAXHandler* handler, void* user_data); +int taurus_sax_parser_feed(TaurusSAXParser* parser, const char* xml, + size_t len, int is_final); +void taurus_sax_parser_free(TaurusSAXParser* parser); +``` + +## Implementation + +```ruby +module Taurus + module XML + module SAX + class Document + def start_document; end + def end_document; end + def start_element(name, attrs = []); end + def end_element(name); end + def characters(string); end + def comment(string); end + def cdata(string); end + def processing_instruction(name, content); end + def start_prefix_mapping(prefix, uri); end + def end_prefix_mapping(prefix); end + def error(message, line, column); end + def warning(message); end + end + + class Parser + def initialize(handler = Document.new) + @handler = handler + end + + def parse(io_or_string) + xml = io_or_string.respond_to?(:read) ? io_or_string.read : io_or_string + + handler_struct = build_handler_struct(@handler) + rc = FFI.taurus_sax_parse(xml, xml.bytesize, handler_struct, nil) + raise ParseError, "SAX parse failed (rc=#{rc})" if rc != 0 + self + end + + private + + def build_handler_struct(handler) + s = FFI::SAXHandler.new + s[:start_document] = make_callback(:start_document) + s[:end_document] = make_callback(:end_document) + s[:start_element] = make_callback(:start_element) + s[:end_element] = make_callback(:end_element) + s[:characters] = make_callback(:characters) + s[:comment] = make_callback(:comment) + s[:cdata] = make_callback(:cdata) + s[:processing_instruction] = make_callback(:processing_instruction) + s[:start_prefix_mapping] = make_callback(:start_prefix_mapping) + s[:end_prefix_mapping] = make_callback(:end_prefix_mapping) + s[:error] = make_callback(:error) + s + end + + def make_callback(event) + ::FFI::Function.new(:void, [:pointer]) do |_user_data| + @handler.send(event) + end + end + end + end + end +end +``` + +## Callback signatures (FFI::Function) + +Each callback needs the right C signature: + +```ruby +# start_document: void(void*) +s[:start_document] = FFI::Function.new(:void, [:pointer]) do + @handler.start_document +end + +# start_element: void(void*, const char*, const char**) +# attrs is a NULL-terminated array of name-value pairs +s[:start_element] = FFI::Function.new(:void, [:pointer, :string, :pointer]) do |_, name, attrs_ptr| + attrs = [] + unless attrs_ptr.null? + offset = 0 + loop do + key_ptr = attrs_ptr.get_pointer(offset) + break if key_ptr.null? + val_ptr = attrs_ptr.get_pointer(offset + FFI.type_size(:pointer)) + break if val_ptr.null? + attrs << key_ptr.read_string + attrs << val_ptr.read_string + offset += 2 * FFI.type_size(:pointer) + end + end + @handler.start_element(name, attrs.each_slice(2).to_h) +end + +# characters: void(void*, const char*, size_t) +s[:characters] = FFI::Function.new(:void, [:pointer, :pointer, :size_t]) do |_, text_ptr, len| + @handler.characters(text_ptr.read_bytes(len).force_encoding('UTF-8')) +end + +# error: void(void*, const char*, int, int) +s[:error] = FFI::Function.new(:void, [:pointer, :string, :int, :int]) do |_, msg, line, col| + @handler.error(msg, line, col) +end +``` + +## Streaming (incremental) parsing + +For large documents, use `taurus_sax_parser_create` + `feed`: + +```ruby +def parse(io) + return parse(io.read) unless io.respond_to?(:read) + + handler_struct = build_handler_struct(@handler) + parser = FFI.taurus_sax_parser_create(handler_struct, nil) + + io.each_chunk(4096) do |chunk| + rc = FFI.taurus_sax_parser_feed(parser, chunk, chunk.bytesize, 0) + break if rc != 0 + end + # Final flush + FFI.taurus_sax_parser_feed(parser, '', 0, 1) +ensure + FFI.taurus_sax_parser_free(parser) if parser +end +``` + +## File + +``` +lib/taurus/xml/sax.rb +lib/taurus/xml/sax/parser.rb +lib/taurus/xml/sax/document.rb +``` + +## Autoload + +```ruby +# lib/taurus/xml/sax.rb +module Taurus + module XML + module SAX + autoload :Parser, 'taurus/xml/sax/parser' + autoload :Document, 'taurus/xml/sax/document' + end + end +end +``` diff --git a/TODO.impl/05-serialize-c14n-memory-specs-css.md b/TODO.impl/05-serialize-c14n-memory-specs-css.md new file mode 100644 index 0000000..b653a0e --- /dev/null +++ b/TODO.impl/05-serialize-c14n-memory-specs-css.md @@ -0,0 +1,276 @@ +# TODO 5 — Serialization, C14N, memory management, specs, CSS + +## Serialization + +Wrap `taurus_serialize_document` and `taurus_c14n_canonicalize`. + +```ruby +# lib/taurus/xml/serialize_options.rb +class Taurus::XML::SerializeOptions < FFI::Struct + layout \ + :indent, :int, + :xml_declaration, :int, + :no_empty_tags, :int, + :preserve_whitespace, :int +end + +# Document#to_xml +def to_xml(options = {}) + opts = SerializeOptions.new + opts[:indent] = options[:indent] || 0 + opts[:xml_declaration] = options[:no_decl] ? 0 : 1 + opts[:no_empty_tags] = options[:no_empty_tags] ? 1 : 0 + opts[:preserve_whitespace] = options[:preserve_whitespace] ? 1 : 0 + ptr = FFI.taurus_serialize_document(@c_ptr, opts.pointer) + return '' if ptr.nil? || ptr.null? + str = ptr.read_string + FFI.taurus_free_string(ptr) + str +end + +# Node#to_xml (serialize just this subtree) +def to_xml(options = {}) + # No C API for single-node serialization yet. Use Document serialize + # with a filter, or build the string manually. For v0.4.2, use the + # document-level serialize and post-process. This is a known limitation. + document.to_xml(options) +end + +# Node#inner_html +def inner_html + children.map { |c| c.to_xml }.join +end +``` + +## C14N (Canonical XML) + +```ruby +# lib/taurus/xml/c14n.rb +module Taurus::XML + C14N_1_0 = 0 + C14N_1_1 = 1 + C14N_EXCLUSIVE = 2 + + class Document + def canonicalize(mode = C14N_1_0, with_comments = false) + ptr = FFI.taurus_c14n_canonicalize(@c_ptr, mode, with_comments ? 1 : 0) + return '' if ptr.nil? || ptr.null? + str = ptr.read_string + FFI.taurus_free_string(ptr) + str + end + end +end +``` + +## Memory management + +### Ownership rules + +| Ruby class | Owns C memory? | Free function | +|-------------|----------------|---------------| +| Document | YES | `taurus_document_free` | +| Node/Element| NO (borrowed) | none (freed by Document) | +| NodeSet | YES (XPath result) | `taurus_xpath_result_free` | +| Attr | NO (borrowed) | none | + +### Explicit free pattern + +```ruby +doc = Taurus::XML.parse(xml) +begin + # ... work with doc ... +ensure + doc.free +end +``` + +### GC safety net + +```ruby +class Taurus::XML::Document + def self.wrap(ptr) + obj = allocate + obj.instance_variable_set(:@c_ptr, ptr) + ObjectSpace.define_finalizer(obj, finalizer(ptr)) + obj + end + + def self.finalizer(ptr) + proc { FFI.taurus_document_free(ptr) if ptr && !ptr.null? } + end + + def free + return unless @c_ptr + FFI.taurus_document_free(@c_ptr) + @c_ptr = nil + # The finalizer still holds the old pointer but Document#free + # already freed it. Add a "freed" flag to detect double-free. + end +end +``` + +**IMPORTANT**: The finalizer must capture the POINTER VALUE, not the +Document object (which would prevent GC). Use `FFI::Pointer` directly +in the finalizer closure. + +### Prevent use-after-free + +```ruby +class Taurus::XML::Node + def c_ptr + raise UseAfterFreeError, "document has been freed" unless @document.c_ptr + @c_ptr + end +end +``` + +## Specs + +### Structure + +``` +spec/ + spec_helper.rb + xml/ + parse_spec.rb + document_spec.rb + node_spec.rb + element_spec.rb + node_set_spec.rb + xpath_spec.rb + sax_spec.rb + serialize_spec.rb + c14n_spec.rb + memory_spec.rb + fixtures/ + basic.xml + catalog.xml + namespaces.xml +``` + +### Test against Nokogiri behavior + +Where Nokogiri's behavior is well-defined, match it exactly. The +specs should test: + +```ruby +# Parse +doc = Taurus::XML.parse('text') +expect(doc.root.name).to eq('root') +expect(doc.root.children.first['id']).to eq('1') + +# XPath +doc = Taurus::XML.parse('') +expect(doc.xpath('count(//book)')).to eq(2.0) +expect(doc.xpath('//book').length).to eq(2) +expect(doc.at_xpath('//book')).to be_a(Taurus::XML::Element) + +# Search +doc = Taurus::XML.parse('') +expect(doc.search('a').length).to eq(2) +expect(doc.at('a')['class']).to eq('x') + +# SAX +class Handler < Taurus::XML::SAX::Document + attr_reader :elements + def initialize; @elements = []; end + def start_element(name, attrs = []); @elements << name; end +end + +h = Handler.new +Taurus::XML::SAX::Parser.new(h).parse('') +expect(h.elements).to eq(['r', 'a', 'b']) + +# Serialize +doc = Taurus::XML.parse('') +expect(doc.to_xml).to match(//) + +# C14N +expect(doc.canonicalize).to include('') + +# Memory +doc = Taurus::XML.parse('') +doc.free +expect { doc.root }.to raise_error(Taurus::XML::UseAfterFreeError) +``` + +### Conformance + +Run Nokogiri's own test suite against the Taurus binding where +possible. Skip tests for features Taurus doesn't support (HTML5, +XSLT, RelaxNG, DTD validation beyond what libtaurus provides). + +## CSS-to-XPath converter (minimal) + +```ruby +# lib/taurus/xml/css_to_xpath.rb +module Taurus::XML + module CssToXPath + def self.convert(rule) + parts = rule.strip.split(/\s+/) + xpath_parts = parts.map { |p| convert_part(p) } + '//' + xpath_parts.join('/') + end + + def self.convert_part(part) + # tag → tag + # .class → *[contains(concat(' ', @class,' '),' class ')] + # #id → *[@id='id'] + # [attr] → *[@attr] + # [attr=val] → *[@attr='val'] + # > child handled by split + # :first-child → *[position()=1] + # :last-child → *[position()=last()] + return '*' if part == '*' + + if part.start_with?('.') + cls = part[1..] + "*[contains(concat(' ',normalize-space(@class),' '),' #{cls} ')]" + elsif part.start_with?('#') + id = part[1..] + "*[@id='#{id}']" + elsif match = part.match(/^(\w+)\[(\w+)='?([^'\]]+)'?\]$/i) + "#{match[1]}[@#{match[2]}='#{match[3]}']" + elsif match = part.match(/^(\w+)\[(\w+)\]$/i) + "#{match[1]}[@#{match[2]}]" + elsif match = part.match(/^(\w+):first-child$/i) + "#{match[1]}[position()=1]" + elsif match = part.match(/^(\w+):last-child$/i) + "#{match[1]}[position()=last()]" + else + part # pass through as tag name + end + end + end +end +``` + +This is a minimal converter. For full CSS3 support, integrate the +`css_parser` gem or port Nokogiri's CSS parser. + +## File layout summary + +``` +lib/taurus.rb +lib/taurus/xml.rb +lib/taurus/xml/ + ffi.rb + document.rb + node.rb + element.rb + text.rb + comment.rb + cdata.rb + processing_instruction.rb + attr.rb + node_set.rb + searchable.rb + parse_options.rb + serialize_options.rb + css_to_xpath.rb + sax.rb + sax/ + parser.rb + document.rb +``` diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..03413c3 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,168 @@ +# taurus-ruby vs Nokogiri — Ruby-level benchmarks + +Compares `Taurus::XML` (FFI → libtaurus v0.12.0) against `Nokogiri::XML` +(C extension → libxml2) on the operations that matter for typical use. + +Run with: + +``` +bundle exec ruby -Ilib benchmark/taurus_vs_nokogiri.rb +``` + +## Latest run (M1, libtaurus v0.12.0, Nokogiri 1.19.4) + +All libtaurus upstream issues closed (#166–#262, 21 issues total). +v0.12.0 ships the [#262](https://github.com/lutaml/taurus/issues/262) proposals: +batch XPath result accessor (`taurus_xpath_result_get_nodes`) and per-node +`binding_wrapper` field. The Ruby binding uses the batch accessor in +`NodeSet#each`; the `binding_wrapper` is left for the libtaurus team's +other bindings (Python ctypes, etc.) — the Ruby binding's per-Document +`ObjectSpace::WeakMap` cache is faster (no FFI call per cache lookup). + +Median of 3 runs (run-to-run variance is significant on cold starts and +shared-runner workloads; the trend is stable): + +| Operation | Taurus | Nokogiri | Taurus / Nokogiri | Winner | +|---|---:|---:|---:|---| +| Parse small (431 B) | ~6 µs | ~14 µs | **~2×** | Taurus (variable) | +| Parse medium (12 KB) | ~32 µs | ~200 µs | **~6×** | Taurus | +| XPath `count(//book)` | ~1.5 µs | ~9 µs | **~6×** | Taurus | +| XPath `//book` (100-node nodeset) | ~4 µs | ~13 µs | **~5×** | Taurus | +| XPath `//book[@id='50']` (1 match) | ~6 µs | ~80 µs | **~10×** | Taurus | +| XPath `//book[price > 50]` | ~80 µs | ~100 µs | **~1.3×** | Taurus | +| XPath `//author \| //title` (union) | ~14 µs | ~25 µs | **~3×** | Taurus | +| Tree traversal | ~900 µs | ~500 µs | 0.55× | Nokogiri (1.8× faster) | +| Serialize | ~28 µs | ~85 µs | **~3×** | Taurus | + +**Taurus beats Nokogiri on 8 of 9 operations.** Only tree traversal still +loses, by ~1.8×. + +### Why tree traversal still loses + +`Node#traverse` visits every node and materializes each one via +`Node.wrap`. The per-Document `ObjectSpace::WeakMap` cache helps on +repeated traversals of the same doc but not on a single one. Each +visited node pays: +- 1 FFI call to `taurus_node_first_child` / `_next_sibling` +- 1 FFI call to `taurus_node_get_type` (for wrap dispatch) +- 1 Ruby object allocation (cache miss) + +Nokogiri's libxml2 C extension handles traversal in C and only crosses +into Ruby when the user's block is called. No per-node FFI. + +The libtaurus `binding_wrapper` field shipped in v0.12.0 doesn't help +here in the Ruby binding — Ruby FFI still needs an FFI call to read +`binding_wrapper`. The Ruby-side `WeakMap` cache avoids that FFI call +on cache hits. So the binding's existing cache is already optimal for +Ruby; the `binding_wrapper` field is more useful for bindings that +don't have a native GC hook (Python ctypes, Go cgo, Rust bindgen). + +## What changed from earlier runs + +### v0.11.0 → v0.11.2 (binding-side) + +- **Lazy NodeSet** — `NodeSet.from_result` keeps the + `TaurusXPathResult*` alive (via `FFI::AutoPointer`) and materializes + `self[i]` on demand. Eager materialization was the #1 cost. +- **Per-Document wrapper cache** — `ObjectSpace::WeakMap` keyed on c_ptr + address. Eliminates re-allocation on repeated access to the same node. + +### v0.11.4 (libtaurus-side) + +- **`taurus_xpath_result_get_nodes`** — batch accessor. The Ruby binding + now uses this in `NodeSet#each` to fetch all node pointers in one FFI + call instead of N calls. + +### v0.12.0 (libtaurus-side) + +- **`binding_wrapper` field on `TaurusNode`** — present in the C struct + but not used by the Ruby binding (see "Why tree traversal still loses" + above). Useful for non-Ruby bindings. +- **`#261` fix** — `benchmark-ips` on 38 KB docs no longer segfaults. + All upstream issues closed. + +### Before/after the binding + libtaurus v0.12.0 optimizations + +| Operation | v0.11.0 (eager) | v0.12.0 (lazy + batch) | Speedup | +|---|---:|---:|---:| +| XPath `//book` (100 nodes) | 87.77 µs (0.15×) | ~4 µs (5×) | **22×** | +| XPath union (200 nodes) | 188.40 µs (0.14×) | ~14 µs (3×) | **13×** | +| XPath complex | 126.88 µs (0.67×) | ~80 µs (1.3×) | 1.6× | +| Tree traversal | 1203 µs (0.50×) | ~900 µs (0.55×) | 1.3× | + +## What this means for the v0.1.0 release + +- Taurus is the right choice for almost every Nokogiri workload on + small-to-medium docs (≤20 KB): 2–10× faster than Nokogiri. +- Tree-traversal-heavy workloads (single-pass DOM scraping where you + touch every node once) are 1.8× slower than Nokogiri. Acceptable + for v0.1.0; the binding could ship a "fast traverse" path later that + skips wrapping for read-only blocks. +- All known libtaurus bugs are fixed. No upstream blockers. + +## Analysis + +### Where Taurus wins + +**Parse (1.69×–6.90×)** — libtaurus's single-pass direct parser (the only +parser since v0.11.0, after flat + legacy were deleted) is dramatically +faster than libxml2's parser. The gap widens with document size. + +**XPath returning scalars (4.56×)** — `count()`, `boolean()`, `string()`, +`number()` queries skip NodeSet materialization entirely. libtaurus's XPath +bytecode VM evaluates these in a single C call, no Ruby objects allocated +per match. + +**XPath predicate match (13.01×)** — when the predicate narrows to a small +result set (single match in this test), Taurus is much faster than +Nokogiri/libxml2. + +**Serialize (2.84×)** — single C call into `taurus_document_serialize`, +no Ruby traversal. + +### Where Nokogiri wins + +**Nodeset-returning XPath (0.13×–0.16×)** — when a query returns a 100-node +NodeSet, Taurus materializes every node eagerly (100× `Node.wrap` calls, +each dispatching on `taurus_node_get_type`). Nokogiri caches wrappers +lazily. + +**Tree traversal (0.49×)** — same root cause. `Node#traverse` creates a +new wrapper per visited node via `Node.wrap`; Nokogiri reuses cached +wrappers. + +### Optimization opportunities (Ruby-side, no libtaurus work needed) + +1. **Lazy NodeSet materialization.** Currently `NodeSet.from_result` + iterates the C result and calls `Node.wrap` for each entry on + construction. Switch to lazy: keep the `TaurusXPathResult*` alive, + materialize `self[i]` on demand. Frees the eager 100× wrap. +2. **Node wrapper cache.** Weak-ref map keyed on the c_ptr address. + `Node.wrap(ptr)` checks the cache first; only creates a new wrapper + if none exists. Matches Nokogiri's behavior. +3. **Specialized traverse path.** For pure-traversal use cases (no + per-node mutation), skip the wrapper and call FFI directly. Lower + overhead but less idiomatic. + +### Blockers + +**Parse-loop segfault on >20 KB docs with explicit `Document#free` +(libtaurus #256).** The v0.11.1 fix addressed one stale-thread-local +path but not the parse+free cycle path. Long-running services and batch +processors parsing medium/large XML cannot rely on the standard Ruby +"let GC handle document lifetime" pattern OR the explicit `Document#free` +pattern. Tracked upstream; workaround: cap doc size or avoid tight loops. + +**DOCTYPE PUBLIC/SYSTEM not exposed (libtaurus #253).** Unrelated to +benchmarks but blocks 4 Ruby specs. Low impact on perf-sensitive workloads. + +## What this means for the v0.1.0 release + +- For **parse-heavy / XPath-aggregate / serialize** workloads on small-to- + medium docs (≤20 KB): Taurus is clearly the right choice. 1.7-7× faster + than Nokogiri. +- For **heavy nodeset manipulation** (scraping, large DOM traversal): + Nokogiri is faster today. The Ruby-side optimizations above would close + most of the gap. +- For **long-running services on medium/large docs**: blocked by #256 + until libtaurus ships a complete fix. diff --git a/benchmark/taurus_vs_nokogiri.rb b/benchmark/taurus_vs_nokogiri.rb new file mode 100644 index 0000000..0110696 --- /dev/null +++ b/benchmark/taurus_vs_nokogiri.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +# Ruby-level performance comparison: Taurus::XML (FFI → libtaurus v0.12.0) +# vs Nokogiri (C extension → libxml2). +# +# Run with: bundle exec ruby -Ilib benchmark/taurus_vs_nokogiri.rb + +require "benchmark" +require "taurus/xml" +require "nokogiri" + +module Fixtures + SMALL = "" + + (1..10).map { |i| "Book #{i}" }.join + + "" + + MEDIUM = ("" + + (1..100).map do |i| + "" \ + "Book #{i}" \ + "Author #{i}" \ + "#{i}.99" \ + "" + end.join + + "").freeze +end + +def time_it(label, n, &block) + GC.start + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + n.times { yield } + elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0 + us_per_iter = (elapsed / n) * 1_000_000 + printf " %-40s %8.2f µs/iter (%d iters in %.3fs)\n", label, us_per_iter, n, elapsed + us_per_iter +end + +def ratio(label, taurus_us, nokogiri_us) + r = nokogiri_us / taurus_us + who = r > 1 ? "Taurus faster" : "Nokogiri faster" + printf " → %-30s Taurus/Nokogiri = %.2fx (%s)\n\n", label, r, who +end + +N_PARSE_SMALL = 5_000 +N_PARSE_MEDIUM = 1_000 +N_QUERY = 10_000 +N_TRAVERSE = 2_000 +N_SERIALIZE = 1_000 + +puts "===== Parse — small (#{Fixtures::SMALL.bytesize} B) =====" +t = time_it("taurus parse small", N_PARSE_SMALL) { d = Taurus::XML::Document.parse(Fixtures::SMALL); d.free } +n = time_it("nokogiri parse small", N_PARSE_SMALL) { Nokogiri::XML(Fixtures::SMALL) } +ratio("parse small", t, n) + +puts "===== Parse — medium (#{Fixtures::MEDIUM.bytesize} B) =====" +t = time_it("taurus parse medium", N_PARSE_MEDIUM) { d = Taurus::XML::Document.parse(Fixtures::MEDIUM); d.free } +n = time_it("nokogiri parse medium", N_PARSE_MEDIUM) { Nokogiri::XML(Fixtures::MEDIUM) } +ratio("parse medium", t, n) + +# Pre-parse medium for query benchmarks +doc_t = Taurus::XML::Document.parse(Fixtures::MEDIUM) +doc_n = Nokogiri::XML(Fixtures::MEDIUM) + +puts "===== XPath — count(//book) =====" +t = time_it("taurus xpath count()", N_QUERY) { doc_t.xpath("count(//book)") } +n = time_it("nokogiri xpath count()", N_QUERY) { doc_n.xpath("count(//book)") } +ratio("xpath count()", t, n) + +puts "===== XPath — //book (nodeset of 100) =====" +t = time_it("taurus xpath //book", N_QUERY) { doc_t.xpath("//book") } +n = time_it("nokogiri xpath //book", N_QUERY) { doc_n.xpath("//book") } +ratio("xpath //book", t, n) + +puts "===== XPath — predicate //book[@id='50'] =====" +t = time_it("taurus xpath predicate", N_QUERY) { doc_t.xpath("//book[@id='50']") } +n = time_it("nokogiri xpath predicate", N_QUERY) { doc_n.xpath("//book[@id='50']") } +ratio("xpath predicate", t, n) + +puts "===== XPath — complex //book[price > 50] =====" +t = time_it("taurus xpath complex", N_QUERY) { doc_t.xpath("//book[price > 50]") } +n = time_it("nokogiri xpath complex", N_QUERY) { doc_n.xpath("//book[price > 50]") } +ratio("xpath complex", t, n) + +puts "===== XPath — union //author | //title =====" +t = time_it("taurus xpath union", N_QUERY) { doc_t.xpath("//author | //title") } +n = time_it("nokogiri xpath union", N_QUERY) { doc_n.xpath("//author | //title") } +ratio("xpath union", t, n) + +puts "===== Tree traversal (root.traverse) =====" +t = time_it("taurus traverse", N_TRAVERSE) { doc_t.root.traverse { |n| n.name } } +n = time_it("nokogiri traverse", N_TRAVERSE) { doc_n.root.traverse { |n| n.name } } +ratio("traverse", t, n) + +puts "===== Serialize — Document#to_xml =====" +t = time_it("taurus serialize", N_SERIALIZE) { doc_t.to_xml } +n = time_it("nokogiri serialize", N_SERIALIZE) { doc_n.to_xml } +ratio("serialize", t, n) + +doc_t.free + +puts "" +puts "libtaurus v0.12.0 — all upstream issues closed." +puts "All 176 Ruby specs passing, 0 pending." + + diff --git a/ext/taurus/CMakeLists.txt b/ext/taurus/CMakeLists.txt deleted file mode 100644 index 616a9d3..0000000 --- a/ext/taurus/CMakeLists.txt +++ /dev/null @@ -1,129 +0,0 @@ -# Taurus - Fast XML Parser and XPath Evaluator -# Pure C library with Ruby bindings and CLI -cmake_minimum_required(VERSION 3.15) - -project(taurus - VERSION 0.1.0 - DESCRIPTION "Fast XML parser and XPath evaluator in pure C" - LANGUAGES C -) - -# C99 standard required -set(CMAKE_C_STANDARD 99) -set(CMAKE_C_STANDARD_REQUIRED ON) -set(CMAKE_C_EXTENSIONS OFF) - -# Build options -option(BUILD_SHARED_LIBS "Build shared libraries" ON) -option(BUILD_TESTING "Build tests" ON) -option(BUILD_EXAMPLES "Build examples" OFF) -option(TAURUS_BUILD_CLI "Build CLI tool" ON) -option(TAURUS_BUILD_MAN_PAGES "Generate man pages with asciidoctor" OFF) - -# Configuration -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -# Add cmake modules path -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") - -# Include AdocMan for man page generation -if(TAURUS_BUILD_MAN_PAGES) - include(AdocMan) -endif() - -# Add subdirectories -add_subdirectory(lib) - -if(TAURUS_BUILD_CLI) - add_subdirectory(cli) -endif() - -if(BUILD_TESTING) - enable_testing() - add_subdirectory(test) -endif() - -if(BUILD_EXAMPLES) - add_subdirectory(examples/c) -endif() - -# Install CMake config files -include(CMakePackageConfigHelpers) -include(GNUInstallDirs) - -write_basic_package_version_file( - "${CMAKE_CURRENT_BINARY_DIR}/taurus-config-version.cmake" - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion -) - -configure_package_config_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/taurus-config.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/taurus-config.cmake" - INSTALL_DESTINATION lib/cmake/taurus -) - -install(FILES - "${CMAKE_CURRENT_BINARY_DIR}/taurus-config.cmake" - "${CMAKE_CURRENT_BINARY_DIR}/taurus-config-version.cmake" - DESTINATION lib/cmake/taurus -) - -install(EXPORT taurus-targets - FILE taurus-targets.cmake - NAMESPACE taurus:: - DESTINATION lib/cmake/taurus -) - -# pkg-config file -configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/taurus.pc.in" - "${CMAKE_CURRENT_BINARY_DIR}/taurus.pc" - @ONLY -) - -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/taurus.pc" - DESTINATION lib/pkgconfig -) - -# Generate and install man pages (when building CLI) -if(TAURUS_BUILD_CLI AND TAURUS_BUILD_MAN_PAGES) - # Use AdocMan.cmake to generate man pages from AsciiDoc sources - # CLI man pages (section 1): cli/man/taurus.1.adoc, etc. - set(ADOC_MAN_SOURCES - cli/man/taurus.1.adoc - cli/man/taurus-parse.1.adoc - cli/man/taurus-xpath.1.adoc - cli/man/taurus-format.1.adoc - ) - - foreach(ADOC_SOURCE ${ADOC_MAN_SOURCES}) - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${ADOC_SOURCE}") - add_adoc_man("${CMAKE_CURRENT_SOURCE_DIR}/${ADOC_SOURCE}") - endif() - endforeach() -endif() - -# Generate library API man page (section 5) -if(TAURUS_BUILD_MAN_PAGES) - # Library man page (section 5): lib/man/libtaurus.5.adoc - set(LIB_MAN_SOURCE lib/man/libtaurus.5.adoc) - - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${LIB_MAN_SOURCE}") - add_adoc_man("${CMAKE_CURRENT_SOURCE_DIR}/${LIB_MAN_SOURCE}") - endif() -endif() - -# Summary -message(STATUS "") -message(STATUS "Taurus ${PROJECT_VERSION} Configuration:") -message(STATUS " C Compiler: ${CMAKE_C_COMPILER}") -message(STATUS " C Standard: C${CMAKE_C_STANDARD}") -message(STATUS " Build Type: ${CMAKE_BUILD_TYPE}") -message(STATUS " Shared Libraries: ${BUILD_SHARED_LIBS}") -message(STATUS " Build Tests: ${BUILD_TESTING}") -message(STATUS " Build Examples: ${BUILD_EXAMPLES}") -message(STATUS " Build CLI: ${TAURUS_BUILD_CLI}") -message(STATUS " Generate Man Pages: ${TAURUS_BUILD_MAN_PAGES}") -message(STATUS " Install Prefix: ${CMAKE_INSTALL_PREFIX}") -message(STATUS "") \ No newline at end of file diff --git a/ext/taurus/Makefile b/ext/taurus/Makefile deleted file mode 100644 index e24e7e1..0000000 --- a/ext/taurus/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -# Dummy Makefile - actual build done by CMake -all: - @echo 'Library already built by extconf.rb' -install: - @echo 'Library installed to lib/' -clean: - @echo 'Nothing to clean (use rake clean)' diff --git a/ext/taurus/cmake/modules/AdocMan.cmake b/ext/taurus/cmake/modules/AdocMan.cmake deleted file mode 100644 index bd6b66f..0000000 --- a/ext/taurus/cmake/modules/AdocMan.cmake +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (c) 2021 Ribose Inc. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS -# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -#.adoc: -# add_adoc_man -# ----------- -# -# Convert adoc manual page to troff and install it via the custom target. -# -# Parameters -# ^^^^^^^^^^ -# Required parameter is source with AsciiDoc file. Must have adoc extension with man category prepended, i.e. something like ${CMAKE_SOURCE_DIR}/src/utility.1.adoc -# DST - optional parameter, which overrides where generated man will be stored. -# If not specified then will be automatically set to ${CMAKE_BINARY_DIR}/src/utility.1 -# -# Generated man page will be installed via the target, named man_utility -# - -set(ADOCCOMMAND_FOUND 0) -find_program(ADOCCOMMAND_PATH - NAMES asciidoctor - DOC "Path to AsciiDoc processor. Used to generate man pages from AsciiDoc." -) - -if(NOT EXISTS ${ADOCCOMMAND_PATH}) - message(WARNING "AsciiDoc processor not found, man pages will not be generated. Install asciidoctor or use the CMAKE_PROGRAM_PATH variable.") -else() - set(ADOCCOMMAND_FOUND 1) -endif() - -function(add_adoc_man SRC) - if (NOT ${ADOCCOMMAND_FOUND}) - return() - endif() - - cmake_parse_arguments( - ARGS - "" - "DST" - "" - ${ARGN} - ) - - set(ADOC_EXT ".adoc") - get_filename_component(FILE_NAME ${SRC} NAME) - - # The following procedures check against the expected file name - # pattern: "{name}.{man-number}.adoc", and builds to a - # destination file "{name}.{man-number}". - - # Check SRC extension - get_filename_component(END_EXT ${SRC} LAST_EXT) - string(COMPARE EQUAL ${END_EXT} ${ADOC_EXT} _equal) - if (NOT _equal) - message(FATAL_ERROR "SRC must have ${ADOC_EXT} extension.") - endif() - - # Check man number - get_filename_component(EXTS ${SRC} EXT) - string(REGEX MATCH "^\.([1-9])\.+$" _matches ${EXTS}) - set(MAN_NUM ${CMAKE_MATCH_1}) - if (NOT _matches) - message(FATAL_ERROR "Man file with wrong name pattern: ${FILE_NAME} must be in format {name}.[0-9]${ADOC_EXT}.") - endif() - - # Set target name - get_filename_component(TARGET_NAME ${SRC} NAME_WE) - string(PREPEND TARGET_NAME "man_") - - # Build output path if not specified. - if(NOT DST) - get_filename_component(SRC_PREFIX ${SRC} DIRECTORY) - - # Ensure that SRC_PREFIX is within CMAKE_SOURCE_DIR - if(NOT(SRC_PREFIX MATCHES "^${CMAKE_SOURCE_DIR}")) - message(FATAL_ERROR "Cannot build DST path as SRC is outside of the CMake sources dir.") - endif() - STRING(REGEX REPLACE "^${CMAKE_SOURCE_DIR}/" "" SUBDIR_PATH ${SRC}) - - # Strip '.adoc' from the output subpath - get_filename_component(SUBDIR_PATH_NAME_WLE ${SUBDIR_PATH} NAME_WLE) - get_filename_component(SUBDIR_PATH_DIRECTORY ${SUBDIR_PATH} DIRECTORY) - set(DST "${CMAKE_BINARY_DIR}/${SUBDIR_PATH_DIRECTORY}/${SUBDIR_PATH_NAME_WLE}") - endif() - - # Check conformance of destination file name to pattern - get_filename_component(FILE_NAME_WE ${SRC} NAME_WE) - get_filename_component(MAN_FILE_NAME ${DST} NAME) - if(NOT(MAN_FILE_NAME MATCHES "^${FILE_NAME_WE}.${MAN_NUM}$")) - message(FATAL_ERROR "File name of a man page must be in the format {name}.{man-number}${ADOC_EXT}.") - endif() - - # Ensure destination directory exists - get_filename_component(DEST_DIR ${DST} DIRECTORY) - file(MAKE_DIRECTORY "${DEST_DIR}") - - add_custom_command( - OUTPUT ${DST} - COMMAND ${ADOCCOMMAND_PATH} -b manpage ${SRC} -o ${DST} - DEPENDS ${SRC} - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - COMMENT "Generating man page ${SUBDIR_PATH_DIRECTORY}/${SUBDIR_PATH_NAME_WLE}" - VERBATIM - ) - - add_custom_target("${TARGET_NAME}" ALL DEPENDS ${DST}) - install(FILES ${DST} - DESTINATION "${CMAKE_INSTALL_FULL_MANDIR}/man${MAN_NUM}" - COMPONENT doc - ) -endfunction(add_adoc_man) diff --git a/ext/taurus/cmake/taurus-config.cmake.in b/ext/taurus/cmake/taurus-config.cmake.in deleted file mode 100644 index db7b1a5..0000000 --- a/ext/taurus/cmake/taurus-config.cmake.in +++ /dev/null @@ -1,6 +0,0 @@ -# Taurus CMake Config File -@PACKAGE_INIT@ - -include("${CMAKE_CURRENT_LIST_DIR}/taurus-targets.cmake") - -check_required_components(taurus) \ No newline at end of file diff --git a/ext/taurus/cmake/taurus.pc.in b/ext/taurus/cmake/taurus.pc.in deleted file mode 100644 index 70ae577..0000000 --- a/ext/taurus/cmake/taurus.pc.in +++ /dev/null @@ -1,10 +0,0 @@ -prefix=@CMAKE_INSTALL_PREFIX@ -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: taurus -Description: @PROJECT_DESCRIPTION@ -Version: @PROJECT_VERSION@ -Libs: -L${libdir} -ltaurus -lm -Cflags: -I${includedir} \ No newline at end of file diff --git a/ext/taurus/extconf.rb b/ext/taurus/extconf.rb deleted file mode 100644 index 5e09bc4..0000000 --- a/ext/taurus/extconf.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'mkmf' -require 'fileutils' - -# Extconf for building libtaurus using CMake -# This builds the C library and installs it for FFI access - -ext_dir = __dir__ -lib_dir = File.expand_path('../../lib', ext_dir) -build_dir = File.join(ext_dir, 'build') - -# Create build directory -FileUtils.mkdir_p(build_dir) - -# Detect platform-specific library extension -lib_ext = case RUBY_PLATFORM - when /darwin/ - 'dylib' - when /linux/ - 'so' - when /mingw|mswin/ - 'dll' - else - 'so' - end - -puts "Building libtaurus for #{RUBY_PLATFORM}..." - -# Run CMake to configure the build -Dir.chdir(build_dir) do - # Configure with CMake - disable CLI and tests, only build library - cmake_args = [ - '-DCMAKE_BUILD_TYPE=Release', - '-DBUILD_SHARED_LIBS=ON', - '-DBUILD_TESTING=OFF', - '-DTAURUS_BUILD_CLI=OFF', - '-DTAURUS_BUILD_MAN_PAGES=OFF', - '..' - ] - - unless system('cmake', *cmake_args) - raise "CMake configuration failed" - end - - # Build - unless system('cmake', '--build', '.', '--config', 'Release') - raise "CMake build failed" - end - - # Install library to lib directory where FFI can find it - built_lib = Dir.glob("lib/libtaurus.#{lib_ext}").first || - Dir.glob("Release/libtaurus.#{lib_ext}").first || - Dir.glob("libtaurus.#{lib_ext}").first - - if built_lib && File.exist?(built_lib) - target_lib = File.join(lib_dir, "libtaurus.#{lib_ext}") - FileUtils.cp(built_lib, target_lib) - puts "✓ Installed libtaurus.#{lib_ext} to #{lib_dir}" - else - raise "Could not find built library libtaurus.#{lib_ext}" - end -end - -# Create a dummy Makefile for gem installation compatibility -File.open('Makefile', 'w') do |f| - f.puts "# Dummy Makefile - actual build done by CMake" - f.puts "all:" - f.puts "\t@echo 'Library already built by extconf.rb'" - f.puts "install:" - f.puts "\t@echo 'Library installed to lib/'" - f.puts "clean:" - f.puts "\t@echo 'Nothing to clean (use rake clean)'" -end - -puts "✓ Build configuration complete" \ No newline at end of file diff --git a/ext/taurus/lib/CMakeLists.txt b/ext/taurus/lib/CMakeLists.txt deleted file mode 100644 index e7d995f..0000000 --- a/ext/taurus/lib/CMakeLists.txt +++ /dev/null @@ -1,83 +0,0 @@ -# Taurus Library - Pure C XML parser and XPath evaluator - -# Library source files -set(TAURUS_SOURCES - src/xpath/lexer.c - src/xpath/parser.c - src/xpath/evaluator.c - src/xpath/functions.c - src/taurus.c - src/parse_simple.c - src/element.c - src/attribute.c - src/namespace.c - src/error.c -) - -# Library headers (internal) -set(TAURUS_HEADERS - src/taurus_internal.h - src/xpath/xpath_internal.h - src/xpath/lexer.h - src/xpath/parser.h - src/xpath/evaluator.h - src/xpath/functions.h -) - -# Create library target -add_library(taurus ${TAURUS_SOURCES} ${TAURUS_HEADERS}) - -# Set library properties -set_target_properties(taurus PROPERTIES - VERSION ${PROJECT_VERSION} - SOVERSION ${PROJECT_VERSION_MAJOR} - C_STANDARD 99 - C_STANDARD_REQUIRED ON - C_EXTENSIONS OFF -) - -# Include directories -target_include_directories(taurus - PUBLIC - $ - $ - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src - ${CMAKE_CURRENT_SOURCE_DIR}/src/xpath -) - -# Link libraries -target_link_libraries(taurus PRIVATE m) - -# Compiler flags -target_compile_options(taurus PRIVATE - -Wall - -Wextra - -Wno-unused-parameter - $<$:-O3> - $<$:-g -O0> -) - -# Export symbols (for shared libraries) -if(BUILD_SHARED_LIBS) - target_compile_definitions(taurus PRIVATE TAURUS_BUILDING_DLL) - target_compile_definitions(taurus PUBLIC TAURUS_DLL) -endif() - -# Install library -install(TARGETS taurus - EXPORT taurus-targets - LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib - RUNTIME DESTINATION bin - INCLUDES DESTINATION include -) - -# Install public headers (will create include/taurus/ structure) -install(DIRECTORY include/ - DESTINATION include - FILES_MATCHING PATTERN "*.h" -) - -# Add alias for consistent namespacing -add_library(taurus::taurus ALIAS taurus) \ No newline at end of file diff --git a/ext/taurus/lib/include/taurus.h b/ext/taurus/lib/include/taurus.h deleted file mode 100644 index 604b08b..0000000 --- a/ext/taurus/lib/include/taurus.h +++ /dev/null @@ -1,334 +0,0 @@ -/* libtaurus - Pure C XML/XPath library - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * Public API header - No Ruby dependencies - */ - -#ifndef LIBTAURUS_H -#define LIBTAURUS_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* ============================================================================ - * Opaque Types - Hide implementation details - * ============================================================================ */ - -typedef struct taurus_document* TaurusDocument; -typedef struct taurus_element* TaurusElement; -typedef struct taurus_attribute* TaurusAttribute; -typedef struct taurus_namespace* TaurusNamespace; -typedef struct taurus_xpath_result* TaurusXPathResult; - -/* ============================================================================ - * Status Codes - * ============================================================================ */ - -typedef enum { - TAURUS_OK = 0, - TAURUS_ERROR_MEMORY = -1, /* Memory allocation failed */ - TAURUS_ERROR_PARSE = -2, /* XML parsing error */ - TAURUS_ERROR_XPATH = -3, /* XPath evaluation error */ - TAURUS_ERROR_NULL_ARG = -4, /* NULL argument passed */ - TAURUS_ERROR_INVALID_ARG = -5, /* Invalid argument */ - TAURUS_ERROR_NOT_FOUND = -6 /* Resource not found */ -} TaurusStatus; - -/* ============================================================================ - * XPath Result Types - * ============================================================================ */ - -typedef enum { - TAURUS_XPATH_NODESET, - TAURUS_XPATH_BOOLEAN, - TAURUS_XPATH_NUMBER, - TAURUS_XPATH_STRING -} TaurusXPathResultType; - -/* ============================================================================ - * Document Operations - * ============================================================================ */ - -/** - * Parse XML string into document - * - * @param xml XML string (must be valid UTF-8) - * @param length Length of XML string in bytes - * @param status Output status code (can be NULL) - * @return Document handle or NULL on error - * - * Memory: Caller must call taurus_document_free() when done - * Thread safety: Not thread-safe. One document per thread. - */ -TaurusDocument taurus_parse_string(const char* xml, size_t length, TaurusStatus* status); - -/** - * Free document and all its elements - * - * @param doc Document to free (can be NULL) - */ -void taurus_document_free(TaurusDocument doc); - -/** - * Get root element of document - * - * @param doc Document - * @return Root element or NULL if document is NULL or empty - * - * Memory: Element is owned by document. Do not free separately. - */ -TaurusElement taurus_document_root(TaurusDocument doc); - -/* ============================================================================ - * Element Operations - * ============================================================================ */ - -/** - * Get element name - * - * @param elem Element - * @return Element name or NULL if elem is NULL - * - * Memory: String is owned by element. Do not free or modify. - */ -const char* taurus_element_name(TaurusElement elem); - -/** - * Get element text content (concatenation of all text nodes) - * - * @param elem Element - * @return Text content or NULL if elem is NULL or has no text - * - * Memory: String is owned by element. Do not free or modify. - */ -const char* taurus_element_text(TaurusElement elem); - -/** - * Get attribute value by name - * - * @param elem Element - * @param name Attribute name - * @return Attribute value or NULL if not found - * - * Memory: String is owned by element. Do not free or modify. - */ -const char* taurus_element_attribute(TaurusElement elem, const char* name); - -/** - * Get number of child elements - * - * @param elem Element - * @return Number of children or 0 if elem is NULL - */ -size_t taurus_element_child_count(TaurusElement elem); - -/** - * Get child element by index - * - * @param elem Element - * @param index Child index (0-based) - * @return Child element or NULL if index out of bounds - * - * Memory: Element is owned by document. Do not free separately. - */ -TaurusElement taurus_element_child(TaurusElement elem, size_t index); - -/** - * Get parent element - * - * @param elem Element - * @return Parent element or NULL if elem is root or NULL - * - * Memory: Element is owned by document. Do not free separately. - */ -TaurusElement taurus_element_parent(TaurusElement elem); - -/* ============================================================================ - * Namespace Operations - * ============================================================================ */ - -/** - * Get element's active namespace - * - * @param elem Element - * @return Namespace or NULL if elem has no namespace - * - * Memory: Namespace is owned by element. Do not free separately. - */ -TaurusNamespace taurus_element_namespace(TaurusElement elem); - -/** - * Get namespace URI - * - * @param ns Namespace - * @return URI string or NULL if ns is NULL - * - * Memory: String is owned by namespace. Do not free or modify. - */ -const char* taurus_namespace_uri(TaurusNamespace ns); - -/** - * Get namespace prefix - * - * @param ns Namespace - * @return Prefix string or NULL if default namespace or ns is NULL - * - * Memory: String is owned by namespace. Do not free or modify. - */ -const char* taurus_namespace_prefix(TaurusNamespace ns); - -/** - * Resolve namespace prefix (with inheritance) - * - * @param elem Element to start search from - * @param prefix Prefix to resolve (NULL for default namespace) - * @return Namespace URI or NULL if not found - * - * Memory: String is owned by element. Do not free or modify. - */ -const char* taurus_element_namespace_for_prefix(TaurusElement elem, const char* prefix); - -/* ============================================================================ - * XPath Operations - * ============================================================================ */ - -/** - * Evaluate XPath expression - * - * @param doc Document (required) - * @param context Context element (NULL = document root) - * @param expression XPath expression string - * @return XPath result or NULL on error - * - * Memory: Caller must call taurus_xpath_result_free() when done - * Thread safety: Not thread-safe. One evaluation per thread. - * - * XPath 1.0 compliance: Full XPath 1.0 specification - * - All 13 axes: child, descendant, parent, ancestor, sibling, etc. - * - All 27 functions: string(), count(), position(), etc. - * - All operators: =, !=, <, <=, >, >=, +, -, *, div, mod, |, and, or - * - Predicates: [1], [@attr], [position() > 2], etc. - */ -TaurusXPathResult taurus_xpath_eval( - TaurusDocument doc, - TaurusElement context, - const char* expression -); - -/** - * Get XPath result type - * - * @param result XPath result - * @return Result type or -1 if result is NULL - */ -TaurusXPathResultType taurus_xpath_result_type(TaurusXPathResult result); - -/** - * Get nodeset size (for NODESET results) - * - * @param result XPath result - * @return Number of nodes or 0 if not a nodeset or result is NULL - */ -size_t taurus_xpath_result_count(TaurusXPathResult result); - -/** - * Get node from nodeset by index - * - * @param result XPath result - * @param index Node index (0-based) - * @return Element or NULL if index out of bounds or not a nodeset - * - * Memory: Element is owned by document. Do not free separately. - */ -TaurusElement taurus_xpath_result_get(TaurusXPathResult result, size_t index); - -/** - * Get boolean value (for BOOLEAN results or type conversion) - * - * @param result XPath result - * @return Boolean value (1 = true, 0 = false) - * - * Type conversion rules: - * - BOOLEAN: Direct value - * - NUMBER: true if non-zero and not NaN - * - STRING: true if non-empty - * - NODESET: true if non-empty - */ -int taurus_xpath_result_boolean(TaurusXPathResult result); - -/** - * Get number value (for NUMBER results or type conversion) - * - * @param result XPath result - * @return Number value (NaN if conversion fails) - * - * Type conversion rules: - * - NUMBER: Direct value - * - BOOLEAN: 1.0 or 0.0 - * - STRING: Parsed as number (NaN if invalid) - * - NODESET: First node's string value converted to number - */ -double taurus_xpath_result_number(TaurusXPathResult result); - -/** - * Get string value (for STRING results or type conversion) - * - * @param result XPath result - * @return String value or NULL if result is NULL - * - * Memory: Caller must call taurus_free_string() when done - * - * Type conversion rules: - * - STRING: Direct value - * - BOOLEAN: "true" or "false" - * - NUMBER: String representation of number - * - NODESET: String value of first node (recursive text concatenation) - */ -char* taurus_xpath_result_string(TaurusXPathResult result); - -/** - * Free XPath result - * - * @param result Result to free (can be NULL) - */ -void taurus_xpath_result_free(TaurusXPathResult result); - -/* ============================================================================ - * Memory Management Helpers - * ============================================================================ */ - -/** - * Free string returned by libtaurus - * - * @param str String to free (can be NULL) - * - * Use this to free strings returned by: - * - taurus_xpath_result_string() - */ -void taurus_free_string(char* str); - -/* ============================================================================ - * Version Information - * ============================================================================ */ - -#define LIBTAURUS_VERSION_MAJOR 0 -#define LIBTAURUS_VERSION_MINOR 4 -#define LIBTAURUS_VERSION_PATCH 0 -#define LIBTAURUS_VERSION_STRING "0.4.0" - -/** - * Get libtaurus version string - * - * @return Version string (e.g., "0.4.0") - */ -const char* taurus_version(void); - -#ifdef __cplusplus -} -#endif - -#endif /* LIBTAURUS_H */ \ No newline at end of file diff --git a/ext/taurus/lib/include/taurus/error.h b/ext/taurus/lib/include/taurus/error.h deleted file mode 100644 index ac465e5..0000000 --- a/ext/taurus/lib/include/taurus/error.h +++ /dev/null @@ -1,117 +0,0 @@ -/* error.h - Taurus error handling - * Copyright (c) 2024, Ribose Inc. - * - * Error handling and reporting functions - */ - -#ifndef TAURUS_ERROR_H -#define TAURUS_ERROR_H - -#include "types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/* ============================================================================ - * Error Handling - * ============================================================================ */ - -/** - * @brief Get last error message - * - * Returns a human-readable description of the last error that occurred. - * The string is valid until the next error occurs or the library is - * unloaded. - * - * @return Error message string, or NULL if no error has occurred - * - * @note Thread-safe: Each thread has its own error state - * @note The returned string should not be freed by the caller - */ -TAURUS_API const char* taurus_last_error(void); - -/** - * @brief Clear last error - * - * Clears the last error message. After calling this function, - * taurus_last_error() will return NULL until another error occurs. - */ -TAURUS_API void taurus_clear_error(void); - -/** - * @brief Get error code from parse result - * - * If a parse operation returns NULL, this function can be used to - * determine the specific error code. - * - * @return Error code from taurus_error_code enum - * - * @see taurus_error_code - */ -TAURUS_API taurus_error_code taurus_last_error_code(void); - -/** - * @brief Convert error code to string - * - * Converts an error code to a human-readable string. - * - * @param code Error code to convert - * @return Static string describing the error (never NULL) - * - * @note The returned string is statically allocated and should not be freed - */ -TAURUS_API const char* taurus_error_string(taurus_error_code code); - -/* ============================================================================ - * Parse Error Information - * ============================================================================ */ - -/** - * @brief Get parse error line number - * - * If a parse error occurred, returns the line number where the error - * was detected (1-based). Only valid if track_positions option was enabled. - * - * @return Line number, or 0 if not available - */ -TAURUS_API int taurus_parse_error_line(void); - -/** - * @brief Get parse error column number - * - * If a parse error occurred, returns the column number where the error - * was detected (1-based). Only valid if track_positions option was enabled. - * - * @return Column number, or 0 if not available - */ -TAURUS_API int taurus_parse_error_column(void); - -/** - * @brief Get error context snippet - * - * If an error occurred with context information, returns a snippet of the - * input showing the area around the error (typically ±2 lines). - * - * @return Context snippet string, or NULL if not available - * - * @note The returned string is owned by the error state and should not be freed - * @note The snippet is cleared when taurus_clear_error() is called - */ -TAURUS_API const char* taurus_error_context(void); - -/** - * @brief Get error byte offset - * - * If an error occurred, returns the byte offset in the input where the - * error was detected. - * - * @return Byte offset, or 0 if not available - */ -TAURUS_API size_t taurus_error_byte_offset(void); - -#ifdef __cplusplus -} -#endif - -#endif /* TAURUS_ERROR_H */ \ No newline at end of file diff --git a/ext/taurus/lib/include/taurus/taurus.h b/ext/taurus/lib/include/taurus/taurus.h deleted file mode 100644 index b35e2e0..0000000 --- a/ext/taurus/lib/include/taurus/taurus.h +++ /dev/null @@ -1,472 +0,0 @@ -/* taurus.h - Taurus public API - * Copyright (c) 2024, Ribose Inc. - * - * Pure C XML parser and XPath evaluator - * - * Taurus is a high-performance XML parser with complete XPath 1.0 support, - * designed for speed and standards compliance. It provides: - * - Fast XML parsing (comparable to Ox) - * - Complete XML Namespaces 1.0 support - * - Full XPath 1.0 implementation - * - Zero external dependencies - * - * Basic Usage: - * @code - * #include - * - * const char* xml = "Hello"; - * taurus_document* doc = taurus_parse(xml, strlen(xml)); - * if (!doc) { - * fprintf(stderr, "Parse error: %s\n", taurus_last_error()); - * return; - * } - * - * taurus_element* root = taurus_document_root(doc); - * const char* name = taurus_element_name(root); - * printf("Root: %s\n", name); - * - * taurus_document_free(doc); - * @endcode - */ - -#ifndef TAURUS_H -#define TAURUS_H - -#include -#include "types.h" -#include "error.h" -#include "xpath.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/* ============================================================================ - * Version Information - * ============================================================================ */ - -#define TAURUS_VERSION_MAJOR 0 -#define TAURUS_VERSION_MINOR 5 -#define TAURUS_VERSION_PATCH 0 - -#define TAURUS_VERSION "0.6.0" - -/** - * @brief Get library version string - * - * Returns the version of the Taurus library as a string. - * - * @return Version string (e.g., "0.6.0") - * - * @note The returned string is statically allocated and should not be freed - */ -TAURUS_API const char* taurus_version(void); - -/** - * @brief Get version components - * - * Returns the version of the Taurus library as integers. - * - * @param major Pointer to store major version (can be NULL) - * @param minor Pointer to store minor version (can be NULL) - * @param patch Pointer to store patch version (can be NULL) - */ -TAURUS_API void taurus_version_components(int* major, int* minor, int* patch); - -/* ============================================================================ - * Parse Options - * ============================================================================ */ - -/** - * @brief Initialize parse options with defaults - * - * Sets parse options to their default values: - * - strict: 1 (enabled) - * - preserve_whitespace: 0 (disabled) - * - track_positions: 0 (disabled) - * - * @param opts Options structure to initialize (must not be NULL) - */ -TAURUS_API void taurus_parse_options_init(taurus_parse_options* opts); - -/* ============================================================================ - * Document Functions - * ============================================================================ */ - -/** - * @brief Parse XML string into document - * - * Parses an XML string and returns a document structure. - * Uses default parse options (strict mode, no whitespace preservation). - * - * @param xml XML string to parse (need not be null-terminated) - * @param len Length of XML string in bytes - * @return Document structure on success, NULL on error - * - * @note Caller owns returned document and must free with taurus_document_free() - * @note Use taurus_last_error() to get error details if NULL is returned - * @note The xml string can be freed immediately after parsing - * - * @see taurus_parse_with_options - * @see taurus_document_free - * - * Example: - * @code - * const char* xml = "text"; - * taurus_document* doc = taurus_parse(xml, strlen(xml)); - * if (!doc) { - * fprintf(stderr, "Error: %s\n", taurus_last_error()); - * return; - * } - * // Use document... - * taurus_document_free(doc); - * @endcode - */ -TAURUS_API taurus_document* taurus_parse(const char* xml, size_t len); - -/** - * @brief Parse XML with custom options - * - * Parses an XML string with custom parse options. - * - * @param xml XML string to parse - * @param len Length of XML string - * @param opts Parse options (NULL for defaults) - * @return Document structure on success, NULL on error - * - * @note Caller owns returned document and must free with taurus_document_free() - * - * Example: - * @code - * taurus_parse_options opts; - * taurus_parse_options_init(&opts); - * opts.preserve_whitespace = 1; - * - * taurus_document* doc = taurus_parse_with_options(xml, len, &opts); - * @endcode - */ -TAURUS_API taurus_document* taurus_parse_with_options( - const char* xml, - size_t len, - const taurus_parse_options* opts -); - -/** - * @brief Free document and all its contents - * - * Frees a document and all elements, attributes, and text it contains. - * - * @param doc Document to free (can be NULL) - * - * @note After calling this function, the document pointer and all - * elements/attributes obtained from it are invalid - * @note It is safe to call this function with NULL - */ -TAURUS_API void taurus_document_free(taurus_document* doc); - -/** - * @brief Get root element of document - * - * Returns the root element of a parsed document. - * - * @param doc Document to query (must not be NULL) - * @return Root element, or NULL if document has no root - * - * @note The returned element is owned by the document - * @note Do not free the returned element - */ -TAURUS_API taurus_element* taurus_document_root(taurus_document* doc); - -/** - * @brief Get document encoding - * - * Returns the encoding specified in the XML declaration, if any. - * - * @param doc Document to query (must not be NULL) - * @return Encoding string (e.g., "UTF-8"), or NULL if not specified - * - * @note The returned string is owned by the document - * @note Do not free the returned string - */ -TAURUS_API const char* taurus_document_encoding(taurus_document* doc); - -/* ============================================================================ - * Element Functions - * ============================================================================ */ - -/** - * @brief Get element name - * - * Returns the local name of an element (without namespace prefix). - * - * @param elem Element to query (must not be NULL) - * @return Element name (never NULL, but may be empty string) - * - * @note The returned string is owned by the element - * @note Do not free the returned string - */ -TAURUS_API const char* taurus_element_name(taurus_element* elem); - -/** - * @brief Get element namespace URI - * - * Returns the namespace URI of an element. - * - * @param elem Element to query (must not be NULL) - * @return Namespace URI, or NULL if element has no namespace - * - * @note The returned string is owned by the element - * @note Do not free the returned string - */ -TAURUS_API const char* taurus_element_namespace(taurus_element* elem); - -/** - * @brief Get element namespace prefix - * - * Returns the namespace prefix used in the element's qualified name. - * - * @param elem Element to query (must not be NULL) - * @return Namespace prefix, or NULL if element has no prefix - * - * @note The returned string is owned by the element - * @note Do not free the returned string - */ -TAURUS_API const char* taurus_element_prefix(taurus_element* elem); - -/** - * @brief Get element text content - * - * Returns the concatenated text content of an element and all its descendants. - * - * @param elem Element to query (must not be NULL) - * @return Text content (never NULL, but may be empty string) - * - * @note The returned string is owned by the element - * @note Do not free the returned string - * @note For mixed content, this returns all text nodes concatenated - */ -TAURUS_API const char* taurus_element_text(taurus_element* elem); - -/** - * @brief Get parent element - * - * Returns the parent element of an element. - * - * @param elem Element to query (must not be NULL) - * @return Parent element, or NULL if element is root - * - * @note The returned element is owned by the document - * @note Do not free the returned element - */ -TAURUS_API taurus_element* taurus_element_parent(taurus_element* elem); - -/** - * @brief Get number of child elements - * - * Returns the number of direct child elements. - * - * @param elem Element to query (must not be NULL) - * @return Number of child elements - * - * @note This only counts element children, not text nodes - */ -TAURUS_API size_t taurus_element_child_count(taurus_element* elem); - -/** - * @brief Get child element by index - * - * Returns the child element at the specified index. - * - * @param elem Element to query (must not be NULL) - * @param index Index of child (0-based) - * @return Child element, or NULL if index out of bounds - * - * @note The returned element is owned by the document - * @note Do not free the returned element - */ -TAURUS_API taurus_element* taurus_element_child(taurus_element* elem, size_t index); - -/* ============================================================================ - * Attribute Functions - * ============================================================================ */ - -/** - * @brief Get number of attributes - * - * Returns the number of attributes an element has. - * - * @param elem Element to query (must not be NULL) - * @return Number of attributes - */ -TAURUS_API size_t taurus_element_attribute_count(taurus_element* elem); - -/** - * @brief Get attribute by index - * - * Returns the attribute at the specified index. - * - * @param elem Element to query (must not be NULL) - * @param index Index of attribute (0-based) - * @return Attribute, or NULL if index out of bounds - * - * @note The returned attribute is owned by the element - * @note Do not free the returned attribute - */ -TAURUS_API taurus_attribute* taurus_element_attribute( - taurus_element* elem, - size_t index -); - -/** - * @brief Get attribute value by name - * - * Returns the value of an attribute with the specified name. - * - * @param elem Element to query (must not be NULL) - * @param name Attribute name (must not be NULL) - * @return Attribute value, or NULL if attribute not found - * - * @note The returned string is owned by the element - * @note Do not free the returned string - * @note This searches by local name only (ignores namespaces) - */ -TAURUS_API const char* taurus_element_get_attribute( - taurus_element* elem, - const char* name -); - -/** - * @brief Check if element has attribute - * - * Checks if an element has an attribute with the specified name. - * - * @param elem Element to query (must not be NULL) - * @param name Attribute name (must not be NULL) - * @return 1 if attribute exists, 0 otherwise - */ -TAURUS_API int taurus_element_has_attribute( - taurus_element* elem, - const char* name -); - -/** - * @brief Get attribute name - * - * Returns the local name of an attribute. - * - * @param attr Attribute to query (must not be NULL) - * @return Attribute name (never NULL) - * - * @note The returned string is owned by the attribute - * @note Do not free the returned string - */ -TAURUS_API const char* taurus_attribute_name(taurus_attribute* attr); - -/** - * @brief Get attribute value - * - * Returns the value of an attribute. - * - * @param attr Attribute to query (must not be NULL) - * @return Attribute value (never NULL, but may be empty string) - * - * @note The returned string is owned by the attribute - * @note Do not free the returned string - */ -TAURUS_API const char* taurus_attribute_value(taurus_attribute* attr); - -/** - * @brief Get attribute namespace URI - * - * Returns the namespace URI of an attribute. - * - * @param attr Attribute to query (must not be NULL) - * @return Namespace URI, or NULL if attribute has no namespace - * - * @note The returned string is owned by the attribute - * @note Do not free the returned string - */ -TAURUS_API const char* taurus_attribute_namespace(taurus_attribute* attr); - -/* ============================================================================ - * Namespace Functions - * ============================================================================ */ - -/** - * @brief Get number of namespace declarations - * - * Returns the number of namespace declarations on an element. - * - * @param elem Element to query (must not be NULL) - * @return Number of namespace declarations - * - * @note This only counts declarations on this element, not inherited ones - */ -TAURUS_API size_t taurus_element_namespace_count(taurus_element* elem); - -/** - * @brief Get namespace declaration by index - * - * Returns the namespace declaration at the specified index. - * - * @param elem Element to query (must not be NULL) - * @param index Index of namespace (0-based) - * @return Namespace declaration, or NULL if index out of bounds - * - * @note The returned namespace is owned by the element - * @note Do not free the returned namespace - */ -TAURUS_API taurus_namespace* taurus_element_namespace_decl( - taurus_element* elem, - size_t index -); - -/** - * @brief Resolve namespace prefix - * - * Resolves a namespace prefix to its URI, considering inheritance. - * - * @param elem Element to start search from (must not be NULL) - * @param prefix Namespace prefix to resolve (NULL for default namespace) - * @return Namespace URI, or NULL if prefix not found - * - * @note The returned string is owned by the document - * @note Do not free the returned string - * @note Searches up the element tree to find matching declaration - */ -TAURUS_API const char* taurus_element_resolve_namespace( - taurus_element* elem, - const char* prefix -); - -/** - * @brief Get namespace prefix - * - * Returns the prefix of a namespace declaration. - * - * @param ns Namespace to query (must not be NULL) - * @return Namespace prefix, or NULL for default namespace - * - * @note The returned string is owned by the namespace - * @note Do not free the returned string - */ -TAURUS_API const char* taurus_namespace_prefix(taurus_namespace* ns); - -/** - * @brief Get namespace URI - * - * Returns the URI of a namespace declaration. - * - * @param ns Namespace to query (must not be NULL) - * @return Namespace URI (never NULL) - * - * @note The returned string is owned by the namespace - * @note Do not free the returned string - */ -TAURUS_API const char* taurus_namespace_uri(taurus_namespace* ns); - -#ifdef __cplusplus -} -#endif - -#endif /* TAURUS_H */ \ No newline at end of file diff --git a/ext/taurus/lib/include/taurus/types.h b/ext/taurus/lib/include/taurus/types.h deleted file mode 100644 index 2e678df..0000000 --- a/ext/taurus/lib/include/taurus/types.h +++ /dev/null @@ -1,153 +0,0 @@ -/* types.h - Taurus public type definitions - * Copyright (c) 2024, Ribose Inc. - * - * Public type definitions for libtaurus - */ - -#ifndef TAURUS_TYPES_H -#define TAURUS_TYPES_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* ============================================================================ - * API Export/Import Macros - * ============================================================================ */ - -#if defined(_WIN32) || defined(__CYGWIN__) - #ifdef TAURUS_BUILDING_DLL - #define TAURUS_API __declspec(dllexport) - #elif defined(TAURUS_DLL) - #define TAURUS_API __declspec(dllimport) - #else - #define TAURUS_API - #endif -#else - #if __GNUC__ >= 4 - #define TAURUS_API __attribute__((visibility("default"))) - #else - #define TAURUS_API - #endif -#endif - -/* ============================================================================ - * Opaque Types - * ============================================================================ */ - -/** - * @brief Opaque document structure - * - * Represents a parsed XML document. The internal structure is hidden - * from users to maintain API stability. - */ -typedef struct taurus_document taurus_document; - -/** - * @brief Opaque element structure - * - * Represents an XML element node. The internal structure is hidden - * from users to maintain API stability. - */ -typedef struct taurus_element taurus_element; - -/** - * @brief Opaque attribute structure - * - * Represents an XML attribute. The internal structure is hidden - * from users to maintain API stability. - */ -typedef struct taurus_attribute taurus_attribute; - -/** - * @brief Opaque namespace structure - * - * Represents an XML namespace declaration. The internal structure is hidden - * from users to maintain API stability. - */ -typedef struct taurus_namespace taurus_namespace; - -/** - * @brief Opaque XPath result structure - * - * Represents the result of an XPath evaluation. The internal structure is - * hidden from users to maintain API stability. - */ -typedef struct taurus_xpath_result taurus_xpath_result; - -/* ============================================================================ - * Enumerations - * ============================================================================ */ - -/** - * @brief XPath result type enumeration - * - * Defines the four possible result types from XPath 1.0 evaluation. - */ -typedef enum { - TAURUS_XPATH_BOOLEAN = 0, /**< Boolean result (true/false) */ - TAURUS_XPATH_NUMBER = 1, /**< Number result (double) */ - TAURUS_XPATH_STRING = 2, /**< String result */ - TAURUS_XPATH_NODESET = 3 /**< Node-set result (array of elements) */ -} taurus_xpath_result_type; - -/** - * @brief Error codes - * - * Error codes returned by all Taurus functions. - * Organized by category (parse, XPath, evaluation, generic). - */ -typedef enum { - /* Success */ - TAURUS_OK = 0, /**< Success */ - - /* Parse errors (1xx) - XML parsing issues */ - TAURUS_ERROR_NULL_INPUT = 1, /**< NULL input provided */ - TAURUS_ERROR_EMPTY_INPUT = 2, /**< Empty input provided */ - TAURUS_ERROR_PARSE_FAILED = 3, /**< Parse failed (malformed XML) */ - TAURUS_ERROR_INVALID_XML = 4, /**< Invalid XML structure */ - TAURUS_ERROR_UNCLOSED_TAG = 100, /**< Element tag not closed */ - TAURUS_ERROR_INVALID_ATTR = 101, /**< Invalid attribute syntax */ - TAURUS_ERROR_ENCODING = 102, /**< Encoding error */ - TAURUS_ERROR_NAMESPACE = 103, /**< Namespace error */ - TAURUS_ERROR_MALFORMED = 104, /**< Malformed XML */ - - /* XPath errors (2xx) - Query syntax and semantics */ - TAURUS_ERROR_XPATH_SYNTAX = 200, /**< XPath syntax error */ - TAURUS_ERROR_XPATH_FUNCTION = 201, /**< Unknown or invalid function */ - TAURUS_ERROR_XPATH_TYPE_MISMATCH = 202, /**< Type mismatch in operation */ - TAURUS_ERROR_XPATH_NAMESPACE = 203, /**< Unregistered namespace prefix */ - TAURUS_ERROR_XPATH_UNKNOWN_AXIS = 204, /**< Unknown axis specifier */ - - /* Evaluation errors (3xx) - Runtime issues */ - TAURUS_ERROR_EVAL_CONTEXT = 300, /**< Invalid evaluation context */ - TAURUS_ERROR_EVAL_ARGUMENT = 301, /**< Invalid function argument */ - TAURUS_ERROR_EVAL_OVERFLOW = 302, /**< Numeric overflow */ - - /* Generic errors (9xx) */ - TAURUS_ERROR_OUT_OF_MEMORY = 900, /**< Memory allocation failed */ - TAURUS_ERROR_INTERNAL = 999 /**< Internal error */ -} taurus_error_code; - -/* ============================================================================ - * Options Structures - * ============================================================================ */ - -/** - * @brief XML parse options - * - * Options that control XML parsing behavior. - */ -typedef struct { - int strict; /**< Strict XML validation (1=strict, 0=lenient) */ - int preserve_whitespace; /**< Preserve whitespace-only text nodes */ - int track_positions; /**< Track line/column positions for errors */ -} taurus_parse_options; - -#ifdef __cplusplus -} -#endif - -#endif /* TAURUS_TYPES_H */ \ No newline at end of file diff --git a/ext/taurus/lib/include/taurus/xpath.h b/ext/taurus/lib/include/taurus/xpath.h deleted file mode 100644 index f2441ae..0000000 --- a/ext/taurus/lib/include/taurus/xpath.h +++ /dev/null @@ -1,252 +0,0 @@ -/* xpath.h - Taurus XPath 1.0 API - * Copyright (c) 2024, Ribose Inc. - * - * Complete XPath 1.0 implementation in C - */ - -#ifndef TAURUS_XPATH_H -#define TAURUS_XPATH_H - -#include "types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/* ============================================================================ - * XPath Evaluation - * ============================================================================ */ - -/** - * @brief Evaluate XPath expression against document - * - * Evaluates an XPath 1.0 expression against the given document. - * The expression is evaluated with the document root as the context node. - * - * @param doc Document to evaluate against (must not be NULL) - * @param xpath_expr XPath expression string (must not be NULL) - * @param expr_len Length of expression string - * @return XPath result, or NULL on error - * - * @note Caller owns the returned result and must free it with - * taurus_xpath_result_free() - * @note Use taurus_last_error() to get error details if NULL is returned - * - * @see taurus_xpath_result_free - * @see taurus_xpath_eval_with_context - * - * Example: - * @code - * taurus_document* doc = taurus_parse(xml, strlen(xml)); - * taurus_xpath_result* result = taurus_xpath_eval(doc, "//book", 6); - * if (result) { - * // Use result... - * taurus_xpath_result_free(result); - * } - * taurus_document_free(doc); - * @endcode - */ -TAURUS_API taurus_xpath_result* taurus_xpath_eval( - taurus_document* doc, - const char* xpath_expr, - size_t expr_len -); - -/** - * @brief Evaluate XPath with specific context node - * - * Evaluates an XPath expression with a specific element as the context node. - * This allows evaluating relative paths from any point in the document. - * - * @param doc Document containing the context node - * @param context_node Element to use as context (must be in doc) - * @param xpath_expr XPath expression string - * @param expr_len Length of expression string - * @return XPath result, or NULL on error - * - * @note Caller owns the returned result and must free it with - * taurus_xpath_result_free() - * - * Example: - * @code - * taurus_element* root = taurus_document_root(doc); - * taurus_xpath_result* result = taurus_xpath_eval_with_context( - * doc, root, "./child", 7 - * ); - * @endcode - */ -TAURUS_API taurus_xpath_result* taurus_xpath_eval_with_context( - taurus_document* doc, - taurus_element* context_node, - const char* xpath_expr, - size_t expr_len -); - -/* ============================================================================ - * XPath Result Management - * ============================================================================ */ - -/** - * @brief Free XPath result - * - * Frees all memory associated with an XPath result, including any - * node-sets, strings, or other data contained in the result. - * - * @param result Result to free (can be NULL) - * - * @note After calling this function, the result pointer is invalid - * and should not be used - */ -TAURUS_API void taurus_xpath_result_free(taurus_xpath_result* result); - -/** - * @brief Get result type - * - * Returns the type of an XPath result (boolean, number, string, or node-set). - * - * @param result Result to query (must not be NULL) - * @return Result type - * - * @see taurus_xpath_result_type - */ -TAURUS_API taurus_xpath_result_type taurus_xpath_result_get_type( - const taurus_xpath_result* result -); - -/* ============================================================================ - * Boolean Results - * ============================================================================ */ - -/** - * @brief Get boolean value from result - * - * Returns the boolean value of a result. If the result is not a boolean, - * it is converted according to XPath 1.0 rules: - * - Number: false if 0 or NaN, true otherwise - * - String: false if empty, true otherwise - * - Node-set: false if empty, true otherwise - * - * @param result Result to convert (must not be NULL) - * @return Boolean value (0=false, 1=true) - */ -TAURUS_API int taurus_xpath_result_as_boolean(const taurus_xpath_result* result); - -/* ============================================================================ - * Number Results - * ============================================================================ */ - -/** - * @brief Get number value from result - * - * Returns the numeric value of a result. If the result is not a number, - * it is converted according to XPath 1.0 rules: - * - Boolean: 0.0 if false, 1.0 if true - * - String: parsed as number (NaN if invalid) - * - Node-set: string-value of first node, then parsed - * - * @param result Result to convert (must not be NULL) - * @return Numeric value (may be NaN or infinity) - */ -TAURUS_API double taurus_xpath_result_as_number(const taurus_xpath_result* result); - -/* ============================================================================ - * String Results - * ============================================================================ */ - -/** - * @brief Get string value from result - * - * Returns the string value of a result. If the result is not a string, - * it is converted according to XPath 1.0 rules: - * - Boolean: "true" or "false" - * - Number: number as string - * - Node-set: string-value of first node - * - * @param result Result to convert (must not be NULL) - * @return String value (never NULL, but may be empty string) - * - * @note Caller must free the returned string with free() - * @note Always returns a new allocated string - */ -TAURUS_API char* taurus_xpath_result_as_string(const taurus_xpath_result* result); - -/* ============================================================================ - * Node-set Results - * ============================================================================ */ - -/** - * @brief Get node-set size - * - * Returns the number of nodes in a node-set result. - * If the result is not a node-set, returns 0. - * - * @param result Result to query (must not be NULL) - * @return Number of nodes (0 if not a node-set) - */ -TAURUS_API size_t taurus_xpath_result_nodeset_size( - const taurus_xpath_result* result -); - -/** - * @brief Get node from node-set - * - * Returns the node at the specified index in a node-set result. - * Nodes are returned in document order. - * - * @param result Result to query (must not be NULL) - * @param index Index of node (0-based) - * @return Element at index, or NULL if index out of bounds or not a node-set - * - * @note The returned element is owned by the document, not the result - * @note Do not free the returned element - */ -TAURUS_API taurus_element* taurus_xpath_result_nodeset_get( - const taurus_xpath_result* result, - size_t index -); - -/* ============================================================================ - * XPath Function Support - * ============================================================================ */ - -/** - * @brief Check if XPath function is supported - * - * Checks if a specific XPath function is implemented and available. - * All 27 core XPath 1.0 functions are supported. - * - * @param function_name Function name to check (e.g., "count", "string-length") - * @return 1 if supported, 0 if not - * - * Supported functions: - * - Node-set: last(), position(), count(), id(), local-name(), namespace-uri(), name() - * - String: string(), concat(), starts-with(), contains(), substring-before(), - * substring-after(), substring(), string-length(), normalize-space(), - * translate() - * - Boolean: boolean(), not(), true(), false(), lang() - * - Number: number(), sum(), floor(), ceiling(), round() - */ -TAURUS_API int taurus_xpath_function_supported(const char* function_name); - -/** - * @brief Get list of supported XPath functions - * - * Returns a NULL-terminated array of function names that are supported. - * - * @return Array of function names (static, do not free) - * - * Example: - * @code - * const char** functions = taurus_xpath_supported_functions(); - * for (int i = 0; functions[i] != NULL; i++) { - * printf("%s\n", functions[i]); - * } - * @endcode - */ -TAURUS_API const char** taurus_xpath_supported_functions(void); - -#ifdef __cplusplus -} -#endif - -#endif /* TAURUS_XPATH_H */ \ No newline at end of file diff --git a/ext/taurus/lib/man/libtaurus.5.adoc b/ext/taurus/lib/man/libtaurus.5.adoc deleted file mode 100644 index db5f381..0000000 --- a/ext/taurus/lib/man/libtaurus.5.adoc +++ /dev/null @@ -1,961 +0,0 @@ -= libtaurus(5) -:doctype: manpage -:man manual: Taurus Library Manual -:man source: Taurus 2.0.0 -:man version: 2.0.0 - -== NAME -libtaurus - fast XML parser and XPath evaluator C library - -== SYNOPSIS -[source,c] ----- -#include - -// Compile and link: -// gcc -o app app.c -ltaurus -// Or with pkg-config: -// gcc -o app app.c $(pkg-config --cflags --libs taurus) ----- - -== DESCRIPTION -*libtaurus* is a high-performance C library for parsing XML documents and -evaluating XPath expressions. It provides complete implementations of: - -* XML 1.0 (Fifth Edition) -* XML Namespaces 1.0 (Third Edition) -* XPath 1.0 - -The library is designed for speed and standards compliance with zero external -dependencies. It uses SIMD optimizations where available and maintains minimal -memory overhead. - -=== Key Features - -* Fast XML parsing comparable to Ox -* Complete XPath 1.0 with all 27 functions and 13 axes -* Full namespace support with automatic resolution -* Thread-safe error handling (per-thread error state) -* Zero memory leaks (verified with valgrind) -* No external dependencies (no libxml2) - -== API OVERVIEW - -The API is organized into functional groups: - -* *Document API* - Parsing and document management -* *Element API* - DOM tree navigation and access -* *Attribute API* - Attribute access and iteration -* *Namespace API* - Namespace declaration and resolution -* *XPath API* - XPath expression evaluation -* *Error API* - Error handling and reporting - -== DATA STRUCTURES - -=== taurus_document - -Opaque structure representing a parsed XML document. - -[source,c] ----- -typedef struct taurus_document taurus_document; ----- - -Created by `taurus_parse()`, freed by `taurus_document_free()`. - -=== taurus_element - -Opaque structure representing an XML element node. - -[source,c] ----- -typedef struct taurus_element taurus_element; ----- - -Obtained from document or element navigation functions. Do not free directly. - -=== taurus_attribute - -Opaque structure representing an XML attribute. - -[source,c] ----- -typedef struct taurus_attribute taurus_attribute; ----- - -Obtained from element attribute functions. Do not free directly. - -=== taurus_namespace - -Opaque structure representing a namespace declaration. - -[source,c] ----- -typedef struct taurus_namespace taurus_namespace; ----- - -Obtained from element namespace functions. Do not free directly. - -=== taurus_xpath_result - -Opaque structure representing an XPath evaluation result. - -[source,c] ----- -typedef struct taurus_xpath_result taurus_xpath_result; ----- - -Created by `taurus_xpath_eval()`, freed by `taurus_xpath_result_free()`. - -=== taurus_xpath_result_type - -Enumeration of XPath result types. - -[source,c] ----- -typedef enum { - XPATH_RESULT_NODESET, // Node-set - XPATH_RESULT_BOOLEAN, // Boolean value - XPATH_RESULT_NUMBER, // Numeric value - XPATH_RESULT_STRING // String value -} taurus_xpath_result_type; ----- - -=== taurus_error_code - -Enumeration of error codes. - -[source,c] ----- -typedef enum { - TAURUS_OK = 0, // No error - TAURUS_ERROR_PARSE, // Parse error - TAURUS_ERROR_MEMORY, // Memory allocation failure - TAURUS_ERROR_INVALID_ARG, // Invalid argument - TAURUS_ERROR_XPATH, // XPath evaluation error - TAURUS_ERROR_IO // I/O error -} taurus_error_code; ----- - -=== taurus_parse_options - -Structure for parse options. - -[source,c] ----- -typedef struct { - int strict; // Strict parsing mode (1=on, 0=off) - int preserve_whitespace; // Preserve all whitespace (1=on, 0=off) - int track_positions; // Track line/column positions (1=on, 0=off) -} taurus_parse_options; ----- - -== DOCUMENT API - -=== taurus_parse - -Parse XML string into document. - -[source,c] ----- -taurus_document* taurus_parse(const char* xml, size_t len); ----- - -*Parameters*: - -* `xml` - XML string to parse (need not be null-terminated) -* `len` - Length of XML string in bytes - -*Returns*: Document structure on success, NULL on error - -*Example*: -[source,c] ----- -const char* xml = "Hello"; -taurus_document* doc = taurus_parse(xml, strlen(xml)); -if (!doc) { - fprintf(stderr, "Parse error: %s\n", taurus_last_error()); - return 1; -} -// Use document... -taurus_document_free(doc); ----- - -=== taurus_parse_with_options - -Parse XML with custom options. - -[source,c] ----- -taurus_document* taurus_parse_with_options( - const char* xml, - size_t len, - const taurus_parse_options* opts -); ----- - -*Parameters*: - -* `xml` - XML string to parse -* `len` - Length of XML string -* `opts` - Parse options (NULL for defaults) - -*Returns*: Document structure on success, NULL on error - -=== taurus_document_free - -Free document and all its contents. - -[source,c] ----- -void taurus_document_free(taurus_document* doc); ----- - -*Parameters*: - -* `doc` - Document to free (can be NULL) - -*Notes*: After calling, all elements, attributes, and data from this document -are invalid. - -=== taurus_document_root - -Get root element of document. - -[source,c] ----- -taurus_element* taurus_document_root(taurus_document* doc); ----- - -*Parameters*: - -* `doc` - Document to query (must not be NULL) - -*Returns*: Root element, or NULL if document has no root - -=== taurus_document_encoding - -Get document encoding. - -[source,c] ----- -const char* taurus_document_encoding(taurus_document* doc); ----- - -*Parameters*: - -* `doc` - Document to query (must not be NULL) - -*Returns*: Encoding string (e.g., "UTF-8"), or NULL if not specified - -== ELEMENT API - -=== taurus_element_name - -Get element name. - -[source,c] ----- -const char* taurus_element_name(taurus_element* elem); ----- - -*Parameters*: - -* `elem` - Element to query (must not be NULL) - -*Returns*: Element local name (never NULL, but may be empty) - -=== taurus_element_text - -Get element text content. - -[source,c] ----- -const char* taurus_element_text(taurus_element* elem); ----- - -*Parameters*: - -* `elem` - Element to query (must not be NULL) - -*Returns*: Concatenated text content (never NULL, but may be empty) - -=== taurus_element_parent - -Get parent element. - -[source,c] ----- -taurus_element* taurus_element_parent(taurus_element* elem); ----- - -*Parameters*: - -* `elem` - Element to query (must not be NULL) - -*Returns*: Parent element, or NULL if element is root - -=== taurus_element_child_count - -Get number of child elements. - -[source,c] ----- -size_t taurus_element_child_count(taurus_element* elem); ----- - -*Parameters*: - -* `elem` - Element to query (must not be NULL) - -*Returns*: Number of child elements (not including text nodes) - -=== taurus_element_child - -Get child element by index. - -[source,c] ----- -taurus_element* taurus_element_child(taurus_element* elem, size_t index); ----- - -*Parameters*: - -* `elem` - Element to query (must not be NULL) -* `index` - Index of child (0-based) - -*Returns*: Child element, or NULL if index out of bounds - -*Example*: -[source,c] ----- -size_t count = taurus_element_child_count(root); -for (size_t i = 0; i < count; i++) { - taurus_element* child = taurus_element_child(root, i); - printf("Child %zu: %s\n", i, taurus_element_name(child)); -} ----- - -== ATTRIBUTE API - -=== taurus_element_attribute_count - -Get number of attributes. - -[source,c] ----- -size_t taurus_element_attribute_count(taurus_element* elem); ----- - -*Parameters*: - -* `elem` - Element to query (must not be NULL) - -*Returns*: Number of attributes - -=== taurus_element_attribute - -Get attribute by index. - -[source,c] ----- -taurus_attribute* taurus_element_attribute( - taurus_element* elem, - size_t index -); ----- - -*Parameters*: - -* `elem` - Element to query (must not be NULL) -* `index` - Index of attribute (0-based) - -*Returns*: Attribute, or NULL if index out of bounds - -=== taurus_element_get_attribute - -Get attribute value by name. - -[source,c] ----- -const char* taurus_element_get_attribute( - taurus_element* elem, - const char* name -); ----- - -*Parameters*: - -* `elem` - Element to query (must not be NULL) -* `name` - Attribute name (must not be NULL) - -*Returns*: Attribute value, or NULL if not found - -*Example*: -[source,c] ----- -const char* id = taurus_element_get_attribute(elem, "id"); -if (id) { - printf("ID: %s\n", id); -} ----- - -=== taurus_element_has_attribute - -Check if element has attribute. - -[source,c] ----- -int taurus_element_has_attribute( - taurus_element* elem, - const char* name -); ----- - -*Parameters*: - -* `elem` - Element to query (must not be NULL) -* `name` - Attribute name (must not be NULL) - -*Returns*: 1 if attribute exists, 0 otherwise - -=== taurus_attribute_name - -Get attribute name. - -[source,c] ----- -const char* taurus_attribute_name(taurus_attribute* attr); ----- - -*Parameters*: - -* `attr` - Attribute to query (must not be NULL) - -*Returns*: Attribute name (never NULL) - -=== taurus_attribute_value - -Get attribute value. - -[source,c] ----- -const char* taurus_attribute_value(taurus_attribute* attr); ----- - -*Parameters*: - -* `attr` - Attribute to query (must not be NULL) - -*Returns*: Attribute value (never NULL, but may be empty) - -== NAMESPACE API - -=== taurus_element_namespace - -Get element namespace URI. - -[source,c] ----- -const char* taurus_element_namespace(taurus_element* elem); ----- - -*Parameters*: - -* `elem` - Element to query (must not be NULL) - -*Returns*: Namespace URI, or NULL if element has no namespace - -=== taurus_element_prefix - -Get element namespace prefix. - -[source,c] ----- -const char* taurus_element_prefix(taurus_element* elem); ----- - -*Parameters*: - -* `elem` - Element to query (must not be NULL) - -*Returns*: Namespace prefix, or NULL if no prefix - -=== taurus_element_resolve_namespace - -Resolve namespace prefix to URI. - -[source,c] ----- -const char* taurus_element_resolve_namespace( - taurus_element* elem, - const char* prefix -); ----- - -*Parameters*: - -* `elem` - Element to start search from (must not be NULL) -* `prefix` - Namespace prefix to resolve (NULL for default namespace) - -*Returns*: Namespace URI, or NULL if prefix not found - -*Notes*: Searches up the element tree to find matching declaration. - -== XPATH API - -=== taurus_xpath_eval - -Evaluate XPath expression against document. - -[source,c] ----- -taurus_xpath_result* taurus_xpath_eval( - taurus_document* doc, - const char* xpath_expr, - size_t expr_len -); ----- - -*Parameters*: - -* `doc` - Document to evaluate against (must not be NULL) -* `xpath_expr` - XPath expression string (must not be NULL) -* `expr_len` - Length of expression string - -*Returns*: XPath result, or NULL on error - -*Example*: -[source,c] ----- -taurus_xpath_result* result = taurus_xpath_eval(doc, "//book", 6); -if (!result) { - fprintf(stderr, "XPath error: %s\n", taurus_last_error()); - return 1; -} -// Use result... -taurus_xpath_result_free(result); ----- - -=== taurus_xpath_result_free - -Free XPath result. - -[source,c] ----- -void taurus_xpath_result_free(taurus_xpath_result* result); ----- - -*Parameters*: - -* `result` - Result to free (can be NULL) - -=== taurus_xpath_result_get_type - -Get result type. - -[source,c] ----- -taurus_xpath_result_type taurus_xpath_result_get_type( - const taurus_xpath_result* result -); ----- - -*Parameters*: - -* `result` - Result to query (must not be NULL) - -*Returns*: Result type (nodeset, boolean, number, or string) - -=== taurus_xpath_result_as_boolean - -Get boolean value from result. - -[source,c] ----- -int taurus_xpath_result_as_boolean(const taurus_xpath_result* result); ----- - -*Parameters*: - -* `result` - Result to convert (must not be NULL) - -*Returns*: Boolean value (0=false, 1=true) - -=== taurus_xpath_result_as_number - -Get number value from result. - -[source,c] ----- -double taurus_xpath_result_as_number(const taurus_xpath_result* result); ----- - -*Parameters*: - -* `result` - Result to convert (must not be NULL) - -*Returns*: Numeric value (may be NaN or infinity) - -=== taurus_xpath_result_as_string - -Get string value from result. - -[source,c] ----- -char* taurus_xpath_result_as_string(const taurus_xpath_result* result); ----- - -*Parameters*: - -* `result` - Result to convert (must not be NULL) - -*Returns*: String value (caller must free with `free()`) - -=== taurus_xpath_result_nodeset_size - -Get node-set size. - -[source,c] ----- -size_t taurus_xpath_result_nodeset_size( - const taurus_xpath_result* result -); ----- - -*Parameters*: - -* `result` - Result to query (must not be NULL) - -*Returns*: Number of nodes (0 if not a node-set) - -=== taurus_xpath_result_nodeset_get - -Get node from node-set. - -[source,c] ----- -taurus_element* taurus_xpath_result_nodeset_get( - const taurus_xpath_result* result, - size_t index -); ----- - -*Parameters*: - -* `result` - Result to query (must not be NULL) -* `index` - Index of node (0-based) - -*Returns*: Element at index, or NULL if out of bounds - -*Example*: -[source,c] ----- -taurus_xpath_result* result = taurus_xpath_eval(doc, "//book", 6); -size_t count = taurus_xpath_result_nodeset_size(result); -for (size_t i = 0; i < count; i++) { - taurus_element* book = taurus_xpath_result_nodeset_get(result, i); - printf("Book: %s\n", taurus_element_text(book)); -} -taurus_xpath_result_free(result); ----- - -== ERROR API - -=== taurus_last_error - -Get last error message. - -[source,c] ----- -const char* taurus_last_error(void); ----- - -*Returns*: Error message string, or NULL if no error - -*Notes*: Thread-safe - each thread has its own error state. - -=== taurus_clear_error - -Clear last error. - -[source,c] ----- -void taurus_clear_error(void); ----- - -=== taurus_last_error_code - -Get error code. - -[source,c] ----- -taurus_error_code taurus_last_error_code(void); ----- - -*Returns*: Error code from taurus_error_code enum - -=== taurus_parse_error_line - -Get parse error line number. - -[source,c] ----- -int taurus_parse_error_line(void); ----- - -*Returns*: Line number (1-based), or 0 if not available - -== EXAMPLES - -=== Basic XML Parsing - -[source,c] ----- -#include -#include -#include - -int main(void) { - const char* xml = "Hello"; - - // Parse XML - taurus_document* doc = taurus_parse(xml, strlen(xml)); - if (!doc) { - fprintf(stderr, "Parse error: %s\n", taurus_last_error()); - return 1; - } - - // Access root element - taurus_element* root = taurus_document_root(doc); - printf("Root: %s\n", taurus_element_name(root)); - - // Navigate children - size_t count = taurus_element_child_count(root); - for (size_t i = 0; i < count; i++) { - taurus_element* child = taurus_element_child(root, i); - printf("Child: %s = %s\n", - taurus_element_name(child), - taurus_element_text(child)); - } - - // Cleanup - taurus_document_free(doc); - return 0; -} ----- - -=== DOM Traversal - -[source,c] ----- -#include - -void print_element(taurus_element* elem, int depth) { - // Print indentation - for (int i = 0; i < depth; i++) printf(" "); - - // Print element name - printf("<%s", taurus_element_name(elem)); - - // Print attributes - size_t attr_count = taurus_element_attribute_count(elem); - for (size_t i = 0; i < attr_count; i++) { - taurus_attribute* attr = taurus_element_attribute(elem, i); - printf(" %s=\"%s\"", - taurus_attribute_name(attr), - taurus_attribute_value(attr)); - } - printf(">\n"); - - // Print children recursively - size_t child_count = taurus_element_child_count(elem); - for (size_t i = 0; i < child_count; i++) { - taurus_element* child = taurus_element_child(elem, i); - print_element(child, depth + 1); - } -} - -int main(void) { - const char* xml = ""; - taurus_document* doc = taurus_parse(xml, strlen(xml)); - - print_element(taurus_document_root(doc), 0); - - taurus_document_free(doc); - return 0; -} ----- - -=== XPath Query - -[source,c] ----- -#include - -int main(void) { - const char* xml = - "" - " C Programming" - " XPath Guide" - ""; - - taurus_document* doc = taurus_parse(xml, strlen(xml)); - - // Execute XPath query - taurus_xpath_result* result = - taurus_xpath_eval(doc, "//title", 7); - - // Process results - int type = taurus_xpath_result_get_type(result); - if (type == XPATH_RESULT_NODESET) { - size_t count = taurus_xpath_result_nodeset_size(result); - printf("Found %zu titles:\n", count); - - for (size_t i = 0; i < count; i++) { - taurus_element* elem = - taurus_xpath_result_nodeset_get(result, i); - printf(" - %s\n", taurus_element_text(elem)); - } - } - - taurus_xpath_result_free(result); - taurus_document_free(doc); - return 0; -} ----- - -=== Error Handling - -[source,c] ----- -#include - -int main(void) { - const char* malformed = ""; - - taurus_document* doc = taurus_parse(malformed, strlen(malformed)); - if (!doc) { - printf("Parse failed!\n"); - printf("Error: %s\n", taurus_last_error()); - printf("Code: %d\n", taurus_last_error_code()); - printf("Line: %d\n", taurus_parse_error_line()); - return 1; - } - - taurus_document_free(doc); - return 0; -} ----- - -=== Namespace Handling - -[source,c] ----- -#include - -int main(void) { - const char* xml = - "" - " Example" - ""; - - taurus_document* doc = taurus_parse(xml, strlen(xml)); - taurus_element* root = taurus_document_root(doc); - - // Navigate to title - taurus_element* title = taurus_element_child(root, 0); - - printf("Element: %s\n", taurus_element_name(title)); - printf("Prefix: %s\n", taurus_element_prefix(title)); - printf("Namespace: %s\n", taurus_element_namespace(title)); - - // Resolve namespace - const char* uri = taurus_element_resolve_namespace(title, "book"); - printf("Resolved: %s\n", uri); - - taurus_document_free(doc); - return 0; -} ----- - -== RETURN VALUES - -Most functions return pointers or values directly. NULL return indicates: - -* Object not found (element, attribute functions) -* Error occurred (parse, XPath functions - check `taurus_last_error()`) -* End of collection (child/attribute iteration) - -Functions never return NULL unless documented otherwise. - -== MEMORY MANAGEMENT - -=== Ownership Rules - -1. *Documents*: Caller owns, must free with `taurus_document_free()` -2. *Elements/Attributes*: Owned by document, freed automatically -3. *XPath Results*: Caller owns, must free with `taurus_xpath_result_free()` -4. *Strings from functions*: Owned by object, do not free -5. *Strings from `taurus_xpath_result_as_string()`*: Caller owns, must free with `free()` - -=== Thread Safety - -* Parsing is thread-safe if different documents -* Error state is per-thread (thread-safe) -* XPath evaluation is thread-safe if different documents -* Do not share document pointers between threads - -=== Best Practices - -* Always check return values for NULL -* Free documents and XPath results when done -* Never free elements or attributes directly -* Use `taurus_last_error()` to diagnose failures - -== FILES - -*/usr/local/include/taurus/taurus.h*:: - Main header file - -*/usr/local/include/taurus/xpath.h*:: - XPath API declarations - -*/usr/local/include/taurus/error.h*:: - Error handling API - -*/usr/local/lib/libtaurus.so*:: - Shared library (Linux) - -*/usr/local/lib/libtaurus.dylib*:: - Shared library (macOS) - -*/usr/local/lib/libtaurus.a*:: - Static library - -*/usr/local/lib/pkgconfig/taurus.pc*:: - pkg-config file - -== SEE ALSO - -*taurus*(1), *taurus-xpath*(1), *pkg-config*(1) - -== STANDARDS - -*libtaurus* implements the following W3C specifications: - -* XML 1.0 (Fifth Edition) - https://www.w3.org/TR/xml/ -* XML Namespaces 1.0 (Third Edition) - https://www.w3.org/TR/xml-names/ -* XPath 1.0 - https://www.w3.org/TR/xpath-10/ - -== BUGS - -Report bugs at: https://github.com/lutaml/taurus/issues - -== COPYRIGHT - -Copyright (c) 2024 Ribose Inc. Licensed under the MIT License. - -== AUTHORS - -Developed by Ribose Inc. and contributors. \ No newline at end of file diff --git a/ext/taurus/lib/src/attribute.c b/ext/taurus/lib/src/attribute.c deleted file mode 100644 index a8576ff..0000000 --- a/ext/taurus/lib/src/attribute.c +++ /dev/null @@ -1,101 +0,0 @@ -/* attribute.c - Taurus attribute API implementation - * Copyright (c) 2024, Ribose Inc. - * - * Attribute access functions - */ - -#include "taurus/taurus.h" -#include "taurus_internal.h" -#include - -/* ============================================================================ - * Element Attribute Access Functions - * ============================================================================ */ - -/** - * Get number of attributes - */ -TAURUS_API size_t taurus_element_attribute_count(struct taurus_element* elem) { - if (!elem) return 0; - return elem->attributes_count; -} - -/** - * Get attribute by index - */ -TAURUS_API struct taurus_attribute* taurus_element_attribute( - struct taurus_element* elem, - size_t index -) { - if (!elem) return NULL; - if (index >= elem->attributes_count) return NULL; - return elem->attributes[index]; -} - -/** - * Get attribute value by name - */ -TAURUS_API const char* taurus_element_get_attribute( - struct taurus_element* elem, - const char* name -) { - if (!elem || !name) return NULL; - - /* Search through attributes for matching name */ - for (size_t i = 0; i < elem->attributes_count; i++) { - struct taurus_attribute* attr = elem->attributes[i]; - if (attr && attr->name && strcmp(attr->name, name) == 0) { - return attr->value; - } - } - - return NULL; /* Not found */ -} - -/** - * Check if element has attribute - */ -TAURUS_API int taurus_element_has_attribute( - struct taurus_element* elem, - const char* name -) { - if (!elem || !name) return 0; - - /* Search through attributes for matching name */ - for (size_t i = 0; i < elem->attributes_count; i++) { - struct taurus_attribute* attr = elem->attributes[i]; - if (attr && attr->name && strcmp(attr->name, name) == 0) { - return 1; /* Found */ - } - } - - return 0; /* Not found */ -} - -/* ============================================================================ - * Attribute Property Functions - * ============================================================================ */ - -/** - * Get attribute name - */ -TAURUS_API const char* taurus_attribute_name(struct taurus_attribute* attr) { - if (!attr) return ""; - return attr->name ? attr->name : ""; -} - -/** - * Get attribute value - */ -TAURUS_API const char* taurus_attribute_value(struct taurus_attribute* attr) { - if (!attr) return ""; - return attr->value ? attr->value : ""; -} - -/** - * Get attribute namespace URI - */ -TAURUS_API const char* taurus_attribute_namespace(struct taurus_attribute* attr) { - if (!attr) return NULL; - return attr->namespace_uri; -} \ No newline at end of file diff --git a/ext/taurus/lib/src/chartype.h b/ext/taurus/lib/src/chartype.h deleted file mode 100644 index c2aac94..0000000 --- a/ext/taurus/lib/src/chartype.h +++ /dev/null @@ -1,124 +0,0 @@ -/* chartype.h - Ultra-fast character classification using lookup table - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * Inspired by pugixml's character classification approach. - * Replaces multiple if/else branches with single table lookup + bitwise AND. - * Expected: 10-15% speedup by eliminating branch mispredictions. - */ - -#ifndef TAURUS_CHARTYPE_H -#define TAURUS_CHARTYPE_H - -/* Character type bit flags - can be combined with bitwise OR */ -typedef enum { - ct_whitespace = 1, /* ' ', '\t', '\r', '\n' */ - ct_name_start = 2, /* [A-Za-z_:] */ - ct_name_char = 4, /* [A-Za-z0-9:_.-] */ - ct_parse_pcdata = 8, /* Everything except <, &, \r */ - ct_parse_attr = 16, /* Everything except <, &, \r, ", ' */ - ct_digit = 32, /* [0-9] */ - ct_start_symbol = 64, /* Valid start tag chars (A-Za-z) */ - ct_alpha = 128 /* [A-Za-z] */ -} chartype_t; - -/* Pre-computed character type table (256 bytes, fits in L1 cache) */ -static const unsigned char chartype_table[256] = { - /* 0-31 (control chars) */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, /* \t=1, \n=1, \r=1 */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - - /* 32-47 (space and punctuation) */ - 1, /* 32 ' ' - whitespace */ - 24, /* 33 '!' - pcdata + attr */ - 0, /* 34 '"' - quote (excluded from attr) */ - 24, /* 35 '#' - pcdata + attr */ - 24, /* 36 '$' - pcdata + attr */ - 24, /* 37 '%' - pcdata + attr */ - 0, /* 38 '&' - entity (excluded from pcdata/attr) */ - 0, /* 39 '\'' - quote (excluded from attr) */ - 24, /* 40 '(' - pcdata + attr */ - 24, /* 41 ')' - pcdata + attr */ - 24, /* 42 '*' - pcdata + attr */ - 24, /* 43 '+' - pcdata + attr */ - 24, /* 44 ',' - pcdata + attr */ - 4, /* 45 '-' - name_char only */ - 4, /* 46 '.' - name_char only */ - 24, /* 47 '/' - pcdata + attr */ - - /* 48-57 (digits) */ - 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, /* 0-9: digit + name_char */ - - /* 58-64 (punctuation) */ - 6, /* 58 ':' - name_start + name_char */ - 24, /* 59 ';' - pcdata + attr */ - 0, /* 60 '<' - tag start (excluded from pcdata/attr) */ - 24, /* 61 '=' - pcdata + attr */ - 24, /* 62 '>' - pcdata + attr */ - 24, /* 63 '?' - pcdata + attr */ - 24, /* 64 '@' - pcdata + attr */ - - /* 65-90 (A-Z) */ - 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, /* A-J */ - 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, /* K-T */ - 198, 198, 198, 198, 198, 198, /* U-Z */ - /* 198 = name_start(2) + name_char(4) + start_symbol(64) + alpha(128) */ - - /* 91-96 (punctuation) */ - 24, /* 91 '[' - pcdata + attr */ - 24, /* 92 '\\' - pcdata + attr */ - 24, /* 93 ']' - pcdata + attr */ - 24, /* 94 '^' - pcdata + attr */ - 6, /* 95 '_' - name_start + name_char */ - 24, /* 96 '`' - pcdata + attr */ - - /* 97-122 (a-z) */ - 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, /* a-j */ - 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, /* k-t */ - 198, 198, 198, 198, 198, 198, /* u-z */ - - /* 123-127 (punctuation) */ - 24, 24, 24, 24, 0, /* { | } ~ DEL */ - - /* 128-255 (extended ASCII / UTF-8) */ - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 -}; - -/* Ultra-fast character type test - single table lookup + bitwise AND */ -static inline int is_chartype(unsigned char c, chartype_t ct) { - return chartype_table[c] & ct; -} - -/* Convenience wrappers for common checks */ -static inline int is_whitespace_fast(char c) { - return is_chartype((unsigned char)c, ct_whitespace); -} - -static inline int is_name_start_fast(char c) { - return is_chartype((unsigned char)c, ct_name_start); -} - -static inline int is_name_char_fast(char c) { - return is_chartype((unsigned char)c, ct_name_char); -} - -static inline int is_start_symbol_fast(char c) { - return is_chartype((unsigned char)c, ct_start_symbol); -} - -static inline int is_alpha_fast(char c) { - return is_chartype((unsigned char)c, ct_alpha); -} - -static inline int is_digit_fast(char c) { - return is_chartype((unsigned char)c, ct_digit); -} - -#endif /* TAURUS_CHARTYPE_H */ \ No newline at end of file diff --git a/ext/taurus/lib/src/element.c b/ext/taurus/lib/src/element.c deleted file mode 100644 index deb75df..0000000 --- a/ext/taurus/lib/src/element.c +++ /dev/null @@ -1,57 +0,0 @@ -/* element.c - Taurus element API implementation - * Copyright (c) 2024, Ribose Inc. - * - * Element traversal and access functions - */ - -#include "taurus/taurus.h" -#include "taurus_internal.h" - -/* ============================================================================ - * Element Hierarchy Functions - * ============================================================================ */ - -/** - * Get parent element - */ -TAURUS_API struct taurus_element* taurus_element_parent(struct taurus_element* elem) { - if (!elem) return NULL; - return elem->parent; -} - -/** - * Get number of child elements - */ -TAURUS_API size_t taurus_element_child_count(struct taurus_element* elem) { - if (!elem) return 0; - return elem->children_count; -} - -/** - * Get child element by index - */ -TAURUS_API struct taurus_element* taurus_element_child(struct taurus_element* elem, size_t index) { - if (!elem) return NULL; - if (index >= elem->children_count) return NULL; - return elem->children[index]; -} - -/* ============================================================================ - * Element Namespace Functions - * ============================================================================ */ - -/** - * Get element namespace URI - */ -TAURUS_API const char* taurus_element_namespace(struct taurus_element* elem) { - if (!elem) return NULL; - return elem->namespace_uri; -} - -/** - * Get element namespace prefix - */ -TAURUS_API const char* taurus_element_prefix(struct taurus_element* elem) { - if (!elem) return NULL; - return elem->prefix; -} \ No newline at end of file diff --git a/ext/taurus/lib/src/error.c b/ext/taurus/lib/src/error.c deleted file mode 100644 index 65753fb..0000000 --- a/ext/taurus/lib/src/error.c +++ /dev/null @@ -1,271 +0,0 @@ -/* error.c - Taurus error handling implementation - * Copyright (c) 2024, Ribose Inc. - * - * Error handling with thread-local storage - */ - -#include "taurus/taurus.h" -#include "taurus_internal.h" -#include - -/* ============================================================================ - * Thread-Local Error State - * ============================================================================ */ - -/* Thread-local error state structure */ -typedef struct { - char message[512]; - char context_snippet[256]; /* Context around error position */ - taurus_error_code code; - int line; - int column; - size_t byte_offset; /* Byte offset in input */ -} taurus_error_state; - -/* Thread-local storage for error state */ -#ifdef _WIN32 - __declspec(thread) static taurus_error_state g_error_state = {{0}, {0}, TAURUS_OK, 0, 0, 0}; -#else - static __thread taurus_error_state g_error_state = {{0}, {0}, TAURUS_OK, 0, 0, 0}; -#endif - -/* ============================================================================ - * Internal Error Setting (for use by parser/evaluator) - * ============================================================================ */ - -void taurus_set_error(taurus_error_code code, const char* message) { - g_error_state.code = code; - if (message) { - strncpy(g_error_state.message, message, sizeof(g_error_state.message) - 1); - g_error_state.message[sizeof(g_error_state.message) - 1] = '\0'; - } else { - g_error_state.message[0] = '\0'; - } -} - -void taurus_set_parse_error_position(int line, int column) { - g_error_state.line = line; - g_error_state.column = column; -} - -void taurus_set_error_with_context( - taurus_error_code code, - const char* message, - const char* input, - size_t byte_offset, - int line, - int column -) { - g_error_state.code = code; - g_error_state.line = line; - g_error_state.column = column; - g_error_state.byte_offset = byte_offset; - - /* Copy message */ - if (message) { - strncpy(g_error_state.message, message, sizeof(g_error_state.message) - 1); - g_error_state.message[sizeof(g_error_state.message) - 1] = '\0'; - } else { - g_error_state.message[0] = '\0'; - } - - /* Extract context snippet if input provided */ - if (input) { - taurus_extract_context_snippet( - input, - byte_offset, - line, - g_error_state.context_snippet, - sizeof(g_error_state.context_snippet) - ); - } else { - g_error_state.context_snippet[0] = '\0'; - } -} - -/* Extract context snippet (±2 lines around error position) */ -void taurus_extract_context_snippet( - const char* input, - size_t offset, - int error_line, - char* out_buffer, - size_t buffer_size -) { - if (!input || !out_buffer || buffer_size == 0) { - return; - } - - /* Find current line start */ - const char* current_line_start = input + offset; - while (current_line_start > input && *(current_line_start - 1) != '\n') { - current_line_start--; - } - - /* Find current line end */ - const char* current_line_end = input + offset; - while (*current_line_end && *current_line_end != '\n') { - current_line_end++; - } - - /* Calculate position within line for marker */ - size_t marker_pos = (size_t)(input + offset - current_line_start); - - /* Copy the current line */ - size_t line_len = (size_t)(current_line_end - current_line_start); - - /* Check if we have enough space for line + newline + marker + null */ - size_t needed_space = line_len + 1 + marker_pos + 1 + 1; /* line + \n + spaces + ^ + \0 */ - - if (needed_space > buffer_size) { - /* Not enough space for marker, just copy truncated line */ - size_t max_copy = buffer_size > 1 ? buffer_size - 1 : 0; - if (line_len > max_copy) { - line_len = max_copy; - } - if (line_len > 0) { - memcpy(out_buffer, current_line_start, line_len); - } - out_buffer[line_len] = '\0'; - return; - } - - /* We have enough space - copy line + marker */ - size_t write_pos = 0; - - /* Copy line content */ - memcpy(out_buffer + write_pos, current_line_start, line_len); - write_pos += line_len; - - /* Add newline */ - out_buffer[write_pos++] = '\n'; - - /* Add marker line with spaces and ^ */ - for (size_t i = 0; i < marker_pos; i++) { - out_buffer[write_pos++] = ' '; - } - out_buffer[write_pos++] = '^'; - out_buffer[write_pos] = '\0'; -} - -/* ============================================================================ - * Public Error API - * ============================================================================ */ - -/** - * Get last error message - */ -TAURUS_API const char* taurus_last_error(void) { - if (g_error_state.message[0] == '\0') { - return NULL; - } - return g_error_state.message; -} - -/** - * Clear last error - */ -TAURUS_API void taurus_clear_error(void) { - g_error_state.message[0] = '\0'; - g_error_state.context_snippet[0] = '\0'; - g_error_state.code = TAURUS_OK; - g_error_state.line = 0; - g_error_state.column = 0; - g_error_state.byte_offset = 0; -} - -/** - * Get error code from parse result - */ -TAURUS_API taurus_error_code taurus_last_error_code(void) { - return g_error_state.code; -} - -/** - * Convert error code to string - */ -TAURUS_API const char* taurus_error_string(taurus_error_code code) { - switch (code) { - case TAURUS_OK: - return "Success"; - - /* Parse errors */ - case TAURUS_ERROR_NULL_INPUT: - return "NULL input provided"; - case TAURUS_ERROR_EMPTY_INPUT: - return "Empty input provided"; - case TAURUS_ERROR_PARSE_FAILED: - return "Parse failed (malformed XML)"; - case TAURUS_ERROR_INVALID_XML: - return "Invalid XML structure"; - case TAURUS_ERROR_UNCLOSED_TAG: - return "Element tag not closed"; - case TAURUS_ERROR_INVALID_ATTR: - return "Invalid attribute syntax"; - case TAURUS_ERROR_ENCODING: - return "Encoding error"; - case TAURUS_ERROR_NAMESPACE: - return "Namespace error"; - case TAURUS_ERROR_MALFORMED: - return "Malformed XML"; - - /* XPath errors */ - case TAURUS_ERROR_XPATH_SYNTAX: - return "XPath syntax error"; - case TAURUS_ERROR_XPATH_FUNCTION: - return "Unknown or invalid XPath function"; - case TAURUS_ERROR_XPATH_TYPE_MISMATCH: - return "XPath type mismatch"; - case TAURUS_ERROR_XPATH_NAMESPACE: - return "Unregistered namespace prefix in XPath"; - case TAURUS_ERROR_XPATH_UNKNOWN_AXIS: - return "Unknown XPath axis"; - - /* Evaluation errors */ - case TAURUS_ERROR_EVAL_CONTEXT: - return "Invalid evaluation context"; - case TAURUS_ERROR_EVAL_ARGUMENT: - return "Invalid function argument"; - case TAURUS_ERROR_EVAL_OVERFLOW: - return "Numeric overflow"; - - /* Generic errors */ - case TAURUS_ERROR_OUT_OF_MEMORY: - return "Memory allocation failed"; - case TAURUS_ERROR_INTERNAL: - return "Internal error"; - - default: - return "Unknown error"; - } -} - -/** - * Get parse error line number - */ -TAURUS_API int taurus_parse_error_line(void) { - return g_error_state.line; -} - -/** - * Get parse error column number - */ -TAURUS_API int taurus_parse_error_column(void) { - return g_error_state.column; -} - -/** - * Get error context snippet - */ -TAURUS_API const char* taurus_error_context(void) { - if (g_error_state.context_snippet[0] == '\0') { - return NULL; - } - return g_error_state.context_snippet; -} - -/** - * Get error byte offset - */ -TAURUS_API size_t taurus_error_byte_offset(void) { - return g_error_state.byte_offset; -} \ No newline at end of file diff --git a/ext/taurus/lib/src/namespace.c b/ext/taurus/lib/src/namespace.c deleted file mode 100644 index e0e9e03..0000000 --- a/ext/taurus/lib/src/namespace.c +++ /dev/null @@ -1,92 +0,0 @@ -/* namespace.c - Taurus namespace API implementation - * Copyright (c) 2024, Ribose Inc. - * - * Namespace declaration and resolution functions - */ - -#include "taurus/taurus.h" -#include "taurus_internal.h" -#include - -/* ============================================================================ - * Element Namespace Declaration Functions - * ============================================================================ */ - -/** - * Get number of namespace declarations - */ -TAURUS_API size_t taurus_element_namespace_count(struct taurus_element* elem) { - if (!elem) return 0; - return elem->namespaces_count; -} - -/** - * Get namespace declaration by index - */ -TAURUS_API struct taurus_namespace* taurus_element_namespace_decl( - struct taurus_element* elem, - size_t index -) { - if (!elem) return NULL; - if (index >= elem->namespaces_count) return NULL; - - /* Traverse linked list to find the nth namespace */ - struct taurus_namespace* ns = elem->namespaces; - for (size_t i = 0; i < index && ns; i++) { - ns = ns->next; - } - - return ns; -} - -/** - * Resolve namespace prefix - */ -TAURUS_API const char* taurus_element_resolve_namespace( - struct taurus_element* elem, - const char* prefix -) { - if (!elem) return NULL; - - /* Search up the element tree for matching namespace declaration */ - struct taurus_element* current = elem; - while (current) { - /* Check namespaces on this element */ - struct taurus_namespace* ns = current->namespaces; - while (ns) { - /* Match prefix (NULL matches default namespace) */ - if (prefix == NULL && ns->prefix == NULL) { - return ns->uri; - } - if (prefix && ns->prefix && strcmp(prefix, ns->prefix) == 0) { - return ns->uri; - } - ns = ns->next; - } - - /* Move up to parent */ - current = current->parent; - } - - return NULL; /* Not found */ -} - -/* ============================================================================ - * Namespace Property Functions - * ============================================================================ */ - -/** - * Get namespace prefix - */ -TAURUS_API const char* taurus_namespace_prefix(struct taurus_namespace* ns) { - if (!ns) return NULL; - return ns->prefix; /* NULL for default namespace */ -} - -/** - * Get namespace URI - */ -TAURUS_API const char* taurus_namespace_uri(struct taurus_namespace* ns) { - if (!ns) return ""; - return ns->uri ? ns->uri : ""; -} \ No newline at end of file diff --git a/ext/taurus/lib/src/parse_content.c b/ext/taurus/lib/src/parse_content.c deleted file mode 100644 index 67e424d..0000000 --- a/ext/taurus/lib/src/parse_content.c +++ /dev/null @@ -1,395 +0,0 @@ -/* parse_content.c - Content and namespace processing for XML parser - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * Session 84: Modularized from taurus_parse.c - * Session 85: Added entity reference expansion - * Functions for processing element content, namespace declarations, and attributes - */ - -#include "parse_internal.h" -#include -#include -#include - -/* ================================================================== - * ENTITY REFERENCE EXPANSION - * ================================================================== */ - -/* Built-in XML entities (predefined in XML spec) */ -static struct { - const char* name; - const char* value; -} builtin_entities[] = { - {"lt", "<"}, - {"gt", ">"}, - {"amp", "&"}, - {"apos", "'"}, - {"quot", "\""}, - {NULL, NULL} -}; - -/* Expand entity reference &name; or &#number; or &#xhex; - * Returns expanded string (MUST be freed by caller) or NULL if unknown - * - * Handles: - * - Built-in entities: < > & ' " - * - Decimal character refs: A -> 'A' - * - Hexadecimal character refs: A -> 'A' - */ -char* expand_entity_reference(const char* ref, size_t len) { - size_t i; - char* result; - unsigned long code; - char* endptr; - char number_buf[32]; - - if (!ref || len == 0) return NULL; - - /* Check for character reference: &#... or &#x... */ - if (len >= 2 && ref[0] == '#') { - /* Decimal: A */ - if (ref[1] != 'x' && ref[1] != 'X') { - /* Copy number part to null-terminated buffer */ - if (len - 1 >= sizeof(number_buf)) { - return NULL; /* Number too long */ - } - memcpy(number_buf, ref + 1, len - 1); - number_buf[len - 1] = '\0'; - - /* Parse decimal number */ - code = strtoul(number_buf, &endptr, 10); - if (*endptr != '\0' || code > 0x10FFFF) { - return NULL; /* Invalid number or out of Unicode range */ - } - } - /* Hexadecimal: A or A */ - else { - if (len < 3) return NULL; - - /* Copy hex part to null-terminated buffer */ - if (len - 2 >= sizeof(number_buf)) { - return NULL; /* Number too long */ - } - memcpy(number_buf, ref + 2, len - 2); - number_buf[len - 2] = '\0'; - - /* Parse hexadecimal number */ - code = strtoul(number_buf, &endptr, 16); - if (*endptr != '\0' || code > 0x10FFFF) { - return NULL; /* Invalid number or out of Unicode range */ - } - } - - /* Convert code point to UTF-8 (simplified: only ASCII for now) */ - if (code <= 0x7F) { - result = (char*)taurus_malloc(2); - if (result) { - result[0] = (char)code; - result[1] = '\0'; - } - return result; - } else { - /* TODO: Full UTF-8 encoding for higher code points */ - return NULL; /* For now, only ASCII supported */ - } - } - - /* Named entity reference - check built-in table */ - for (i = 0; builtin_entities[i].name != NULL; i++) { - size_t name_len = strlen(builtin_entities[i].name); - if (len == name_len && memcmp(ref, builtin_entities[i].name, len) == 0) { - /* Found match - return copy of value */ - return taurus_strdup(builtin_entities[i].value); - } - } - - /* Unknown entity */ - return NULL; -} - -/* ================================================================== - * TEXT CONTENT PROCESSING - * ================================================================== */ - -/* Add text content to element with entity expansion */ -void add_text_to_element(struct taurus_element *elem, const char *text, size_t len) { - char *text_copy; - char *expanded_text; - char *new_content; - size_t existing_len; - const char *p, *start; - const char *entity_start, *entity_end; - size_t result_len, result_cap; - char *result; - char *entity_value; - size_t entity_len; - - if (!elem || !text || len == 0) return; - - /* Check if text contains entities (&...) */ - p = text; - entity_start = NULL; - while (p < text + len) { - if (*p == '&') { - entity_start = p; - break; - } - p++; - } - - /* No entities found - simple path */ - if (!entity_start) { - text_copy = taurus_strndup(text, len); - if (!text_copy) return; - - if (!elem->text_content) { - elem->text_content = text_copy; - } else { - existing_len = strlen(elem->text_content); - new_content = (char*)taurus_realloc(elem->text_content, existing_len + len + 1); - if (new_content) { - memcpy(new_content + existing_len, text, len); - new_content[existing_len + len] = '\0'; - elem->text_content = new_content; - taurus_free(text_copy); - } else { - taurus_free(text_copy); - } - } - return; - } - - /* Entities found - expand them */ - result_cap = len + 64; /* Initial capacity with some padding */ - result = (char*)taurus_malloc(result_cap); - if (!result) return; - result_len = 0; - - start = text; - p = text; - - while (p < text + len) { - if (*p == '&') { - /* Copy text before entity */ - if (p > start) { - size_t copy_len = p - start; - if (result_len + copy_len >= result_cap) { - result_cap = (result_len + copy_len + 1) * 2; - result = (char*)taurus_realloc(result, result_cap); - if (!result) return; - } - memcpy(result + result_len, start, copy_len); - result_len += copy_len; - } - - /* Find end of entity (;) */ - entity_start = p + 1; /* Skip '&' */ - entity_end = entity_start; - while (entity_end < text + len && *entity_end != ';') { - entity_end++; - } - - if (entity_end >= text + len || *entity_end != ';') { - /* Unterminated entity - keep as-is */ - if (result_len + 1 >= result_cap) { - result_cap = (result_len + 2) * 2; - result = (char*)taurus_realloc(result, result_cap); - if (!result) return; - } - result[result_len++] = '&'; - start = p + 1; - p++; - continue; - } - - /* Extract and expand entity */ - entity_len = entity_end - entity_start; - entity_value = expand_entity_reference(entity_start, entity_len); - - if (entity_value) { - /* Copy expanded value */ - size_t value_len = strlen(entity_value); - if (result_len + value_len >= result_cap) { - result_cap = (result_len + value_len + 1) * 2; - result = (char*)taurus_realloc(result, result_cap); - if (!result) { - taurus_free(entity_value); - return; - } - } - memcpy(result + result_len, entity_value, value_len); - result_len += value_len; - taurus_free(entity_value); - - /* Move past entity */ - p = entity_end + 1; - start = p; - } else { - /* Unknown entity - keep as-is */ - size_t keep_len = entity_end - p + 1; /* Include & and ; */ - if (result_len + keep_len >= result_cap) { - result_cap = (result_len + keep_len + 1) * 2; - result = (char*)taurus_realloc(result, result_cap); - if (!result) return; - } - memcpy(result + result_len, p, keep_len); - result_len += keep_len; - p = entity_end + 1; - start = p; - } - } else { - p++; - } - } - - /* Copy remaining text */ - if (start < text + len) { - size_t copy_len = (text + len) - start; - if (result_len + copy_len >= result_cap) { - result_cap = result_len + copy_len + 1; - result = (char*)taurus_realloc(result, result_cap); - if (!result) return; - } - memcpy(result + result_len, start, copy_len); - result_len += copy_len; - } - - /* Null-terminate */ - result[result_len] = '\0'; - expanded_text = result; - - /* Add to element */ - if (!elem->text_content) { - elem->text_content = expanded_text; - } else { - existing_len = strlen(elem->text_content); - new_content = (char*)taurus_realloc(elem->text_content, existing_len + result_len + 1); - if (new_content) { - memcpy(new_content + existing_len, expanded_text, result_len); - new_content[existing_len + result_len] = '\0'; - elem->text_content = new_content; - taurus_free(expanded_text); - } else { - taurus_free(expanded_text); - } - } -} - -/* ================================================================== - * NAMESPACE PROCESSING - * ================================================================== */ - -/* Process namespace declarations from attribute stack - * Extracts xmlns and xmlns:prefix attributes and creates namespace objects */ -void process_namespace_declarations(struct taurus_element *elem, TaurusAttrStack *attr_stack) { - size_t i, count; - ParseAttribute *attrs; - const char *name; - struct taurus_namespace *ns; - - if (!elem || !attr_stack) return; - - count = taurus_attr_stack_size(attr_stack); - attrs = taurus_attr_stack_to_array(attr_stack); - - for (i = 0; i < count; i++) { - name = attrs[i].name; - - /* Check if this is a namespace declaration */ - if (strncmp(name, "xmlns", 5) == 0) { - if (name[5] == '\0') { - /* Default namespace: xmlns="uri" */ - char *uri = taurus_strndup(attrs[i].value, strlen(attrs[i].value)); - ns = taurus_namespace_new(NULL, uri); - taurus_free(uri); - if (ns) { - taurus_element_add_namespace(elem, ns); - } - } else if (name[5] == ':') { - /* Prefixed namespace: xmlns:prefix="uri" */ - const char *prefix = name + 6; - char *uri = taurus_strndup(attrs[i].value, strlen(attrs[i].value)); - char *prefix_copy = taurus_strdup(prefix); - ns = taurus_namespace_new(prefix_copy, uri); - taurus_free(uri); - taurus_free(prefix_copy); - if (ns) { - taurus_element_add_namespace(elem, ns); - } - } - } - } -} - -/* Set element namespace from prefix or parent inheritance */ -void set_element_namespace(struct taurus_element *elem, const char *prefix, - struct taurus_element *parent) { - struct taurus_namespace *ns; - - if (!elem) return; - - /* Set prefix if provided */ - if (prefix) { - elem->prefix = taurus_strdup(prefix); - } - - /* Find namespace URI - search element first, then parent if provided - * (parent relationship not yet established at call time) */ - ns = taurus_namespace_find(elem, prefix); - if (!ns && parent) { - /* Not found on element, search parent for inheritance */ - ns = taurus_namespace_find(parent, prefix); - } - - if (ns && ns->uri) { - elem->namespace_uri = taurus_strdup(ns->uri); - } -} - -/* ================================================================== - * ATTRIBUTE PROCESSING - * ================================================================== */ - -/* Add non-xmlns attributes to element */ -void add_attributes_to_element(struct taurus_element *elem, TaurusAttrStack *attr_stack, - StringInternTable *intern_table) { - size_t i, count; - ParseAttribute *attrs; - const char *name; - struct taurus_attribute *attr; - const char *interned_name; - char *value_copy; - - if (!elem || !attr_stack) return; - - count = taurus_attr_stack_size(attr_stack); - attrs = taurus_attr_stack_to_array(attr_stack); - - for (i = 0; i < count; i++) { - name = attrs[i].name; - - /* Skip xmlns declarations (already processed) */ - if (strncmp(name, "xmlns", 5) == 0 && - (name[5] == '\0' || name[5] == ':')) { - continue; - } - - /* Intern attribute name */ - interned_name = string_intern_get(intern_table, name, strlen(name)); - if (!interned_name) { - interned_name = name; /* Fallback */ - } - - /* Create attribute value copy */ - value_copy = taurus_strndup(attrs[i].value, strlen(attrs[i].value)); - - /* Create attribute */ - attr = taurus_attribute_new(interned_name, value_copy); - taurus_free(value_copy); - - if (attr) { - taurus_element_add_attribute(elem, attr); - } - } -} \ No newline at end of file diff --git a/ext/taurus/lib/src/parse_document.c b/ext/taurus/lib/src/parse_document.c deleted file mode 100644 index 737c218..0000000 --- a/ext/taurus/lib/src/parse_document.c +++ /dev/null @@ -1,176 +0,0 @@ -/* parse_document.c - Document-level parsing functions - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * Session 84: Modularized from taurus_parse.c - * Main parsing entry points and document structure creation - */ - -#include "parse_internal.h" -#include - -/* ================================================================== - * PROCESSING INSTRUCTION PARSING - * ================================================================== */ - -/* Parse processing instruction - * Assumes positioned after 'pos, ctx->end); - - /* Find '?>' end marker */ - data_start = ctx->pos; - while (ctx->pos + 1 < ctx->end && - !(ctx->pos[0] == '?' && ctx->pos[1] == '>')) { - if (*ctx->pos == '\n') { - ctx->line++; - } - ctx->pos++; - } - - if (ctx->pos + 1 >= ctx->end) { - taurus_parse_context_set_error(ctx, "Unterminated processing instruction at line %d, column %d", ctx->line, ctx->column); - taurus_free(target); - return NULL; - } - - /* Extract data (between target and '?>') */ - data_len = ctx->pos - data_start; - data = (data_len > 0) ? taurus_strndup(data_start, data_len) : NULL; - - /* Skip '?>' */ - ctx->pos += 2; - - /* Create PI */ - pi = taurus_pi_new(target, data); - taurus_free(target); - if (data) taurus_free(data); - - return pi; -} - -/* ================================================================== - * MAIN PARSE FUNCTIONS - * ================================================================== */ - -/* Parse XML string into document structure */ -struct taurus_document *taurus_parse(const char *xml, - size_t len, - TaurusParseOptions *opts) { - TaurusParseContext ctx; - struct taurus_document *doc; - struct taurus_element *root; - - /* Initialize context */ - if (taurus_parse_context_init(&ctx, xml, len, opts) < 0) { - return NULL; - } - - /* Create document */ - doc = taurus_document_new(); - if (!doc) { - taurus_parse_context_free(&ctx); - return NULL; - } - ctx.doc = doc; - - /* Skip leading whitespace */ - taurus_skip_whitespace(&ctx.pos, ctx.end); - - /* Parse processing instructions (including XML declaration) */ - while (ctx.pos + 1 < ctx.end && - ctx.pos[0] == '<' && ctx.pos[1] == '?') { - ctx.pos += 2; /* Skip 'next = doc->pis; - doc->pis = pi; - - taurus_skip_whitespace(&ctx.pos, ctx.end); - } - - /* Parse root element */ - if (ctx.pos < ctx.end && *ctx.pos == '<') { - ctx.pos++; /* Skip '<' */ - root = parse_element(&ctx, NULL); - if (!root) { - taurus_document_free_internal(doc); - taurus_parse_context_free(&ctx); - return NULL; - } - - /* Set as document root */ - doc->root = root; - } else { - taurus_parse_context_set_error(&ctx, "No root element found at line %d, column %d", ctx.line, ctx.column); - taurus_document_free_internal(doc); - taurus_parse_context_free(&ctx); - return NULL; - } - - /* Cleanup context */ - taurus_parse_context_free(&ctx); - - return doc; -} - -/* Parse XML with error reporting */ -struct taurus_document *taurus_parse_with_error(const char *xml, - size_t len, - TaurusParseOptions *opts, - char *error_buf, - size_t error_len) { - struct taurus_document *doc; - TaurusParseContext ctx; - - /* Initialize context */ - if (taurus_parse_context_init(&ctx, xml, len, opts) < 0) { - if (error_buf && error_len > 0) { - snprintf(error_buf, error_len, "Failed to initialize parse context"); - } - return NULL; - } - - /* Parse document */ - doc = taurus_parse(xml, len, opts); - - /* Copy error if parse failed */ - if (!doc && error_buf && error_len > 0) { - const char *err = taurus_parse_context_error(&ctx); - if (err[0] != '\0') { - snprintf(error_buf, error_len, "%s", err); - } - } - - /* Cleanup */ - taurus_parse_context_free(&ctx); - - return doc; -} \ No newline at end of file diff --git a/ext/taurus/lib/src/parse_element.c b/ext/taurus/lib/src/parse_element.c deleted file mode 100644 index a41f3db..0000000 --- a/ext/taurus/lib/src/parse_element.c +++ /dev/null @@ -1,277 +0,0 @@ -/* parse_element.c - Element parsing functions for XML parser - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * Session 84: Modularized from taurus_parse.c - * Functions for parsing element start tags, end tags, and complete elements - */ - -#include "parse_internal.h" -#include -#include - -/* ================================================================== - * ELEMENT START TAG PARSING - * ================================================================== */ - -/* Parse element start tag - * Returns newly created element or NULL on error - * NOTE: Return value may have lowest bit set to indicate self-closing */ -struct taurus_element *parse_start_tag(TaurusParseContext *ctx, - struct taurus_element *parent) { - struct taurus_element *elem; - const char *name; - size_t name_len, value_len; - char *name_copy, *prefix, *local_name; - int self_closing = 0; - - /* Parse element name */ - name = parse_name(ctx, &name_len); - if (!name) return NULL; - - /* Create a copy for processing */ - name_copy = taurus_strndup(name, name_len); - if (!name_copy) return NULL; - - /* Extract prefix and local name */ - extract_prefix_and_local(name_copy, &prefix, &local_name); - - /* Initialize attribute stack */ - taurus_attr_stack_init(&ctx->attr_stack); - - /* Parse attributes */ - while (ctx->pos < ctx->end) { - taurus_skip_whitespace(&ctx->pos, ctx->end); - - if (ctx->pos >= ctx->end) break; - if (*ctx->pos == '>' || *ctx->pos == '/') break; - - /* Parse attribute name */ - const char *attr_name = parse_name(ctx, &name_len); - if (!attr_name) { - taurus_attr_stack_cleanup(&ctx->attr_stack); - taurus_free(name_copy); - return NULL; - } - - /* Expect '=' */ - taurus_skip_whitespace(&ctx->pos, ctx->end); - if (ctx->pos >= ctx->end || *ctx->pos != '=') { - taurus_parse_context_set_error(ctx, "Expected '=' after attribute name at line %d, column %d", ctx->line, ctx->column); - taurus_attr_stack_cleanup(&ctx->attr_stack); - taurus_free(name_copy); - return NULL; - } - ctx->pos++; - - /* Parse attribute value */ - const char *attr_value = parse_quoted_value(ctx, &value_len); - if (!attr_value) { - taurus_attr_stack_cleanup(&ctx->attr_stack); - taurus_free(name_copy); - return NULL; - } - - /* Push attribute onto stack */ - taurus_attr_stack_push(&ctx->attr_stack, attr_name, attr_value); - } - - /* Check for self-closing */ - taurus_skip_whitespace(&ctx->pos, ctx->end); - if (ctx->pos < ctx->end && *ctx->pos == '/') { - self_closing = 1; - ctx->pos++; /* Skip '/' */ - } - - /* Expect '>' */ - if (ctx->pos >= ctx->end || *ctx->pos != '>') { - taurus_parse_context_set_error(ctx, "Expected '>' at line %d, column %d", ctx->line, ctx->column); - taurus_attr_stack_cleanup(&ctx->attr_stack); - taurus_free(name_copy); - return NULL; - } - ctx->pos++; /* Skip '>' */ - - /* Create element */ - elem = taurus_element_new(local_name); - if (!elem) { - taurus_attr_stack_cleanup(&ctx->attr_stack); - taurus_free(name_copy); - return NULL; - } - - /* Store self-closing flag (we'll use text_content as a hack for now) */ - if (self_closing) { - /* Mark as self-closing - we'll handle this in parse_element */ - } - - /* Process namespace declarations */ - process_namespace_declarations(elem, &ctx->attr_stack); - - /* Set element namespace */ - set_element_namespace(elem, prefix, parent); - - /* Add attributes (excluding xmlns) */ - add_attributes_to_element(elem, &ctx->attr_stack, &ctx->intern_table); - - /* Link to parent */ - if (parent) { - taurus_element_add_child(parent, elem); - } - - /* Cleanup */ - taurus_attr_stack_cleanup(&ctx->attr_stack); - taurus_free(name_copy); - - /* Return element with self_closing status encoded */ - return self_closing ? (struct taurus_element*)((uintptr_t)elem | 1) : elem; -} - -/* ================================================================== - * ELEMENT END TAG PARSING - * ================================================================== */ - -/* Parse element end tag - * Returns 0 on success, -1 on error */ -int parse_end_tag(TaurusParseContext *ctx, const char *expected_name) { - const char *name; - size_t len; - char *name_copy; - const char *local_name; - int match; - - /* Should be at 'pos + 1 >= ctx->end || - ctx->pos[0] != '<' || ctx->pos[1] != '/') { - taurus_parse_context_set_error(ctx, "Expected 'line, ctx->column); - return -1; - } - ctx->pos += 2; - - /* Parse name */ - name = parse_name(ctx, &len); - if (!name) return -1; - - /* Extract local name (remove prefix) */ - name_copy = taurus_strndup(name, len); - if (!name_copy) return -1; - - local_name = extract_local_name_only(name_copy); - - /* Compare with expected */ - match = (strcmp(local_name, expected_name) == 0); - - taurus_free(name_copy); - - if (!match) { - taurus_parse_context_set_error(ctx, - "Mismatched end tag at line %d, column %d: expected %s", - ctx->line, ctx->column, expected_name); - return -1; - } - - /* Expect '>' */ - taurus_skip_whitespace(&ctx->pos, ctx->end); - if (ctx->pos >= ctx->end || *ctx->pos != '>') { - taurus_parse_context_set_error(ctx, - "Expected '>' in end tag at line %d, column %d", ctx->line, ctx->column); - return -1; - } - ctx->pos++; - - return 0; -} - -/* ================================================================== - * COMPLETE ELEMENT PARSING - * ================================================================== */ - -/* Parse complete element (start tag, content, end tag) - * Returns newly created element or NULL on error */ -struct taurus_element *parse_element(TaurusParseContext *ctx, - struct taurus_element *parent) { - struct taurus_element *elem; - uintptr_t elem_ptr; - int self_closing; - - /* Parse start tag */ - elem = parse_start_tag(ctx, parent); - if (!elem) return NULL; - - /* Check if self-closing (encoded in lowest bit) */ - elem_ptr = (uintptr_t)elem; - self_closing = elem_ptr & 1; - elem = (struct taurus_element*)(elem_ptr & ~1); - - /* If self-closing, we're done */ - if (self_closing) { - return elem; - } - - /* Parse content */ - while (ctx->pos < ctx->end) { - taurus_skip_whitespace(&ctx->pos, ctx->end); - - if (ctx->pos >= ctx->end) break; - - if (*ctx->pos == '<') { - ctx->pos++; - - if (ctx->pos >= ctx->end) { - taurus_parse_context_set_error(ctx, "Unexpected end after '<' at line %d, column %d", ctx->line, ctx->column); - taurus_element_free_tree(elem); - return NULL; - } - - if (*ctx->pos == '/') { - /* End tag - rewind and let parse_end_tag handle */ - ctx->pos--; - break; - } else if (ctx->pos + 2 < ctx->end && - *ctx->pos == '!' && ctx->pos[1] == '-' && ctx->pos[2] == '-') { - /* Comment */ - ctx->pos += 3; /* Skip '!--' */ - if (skip_comment(ctx) < 0) { - taurus_element_free_tree(elem); - return NULL; - } - } else if (ctx->pos + 7 < ctx->end && - *ctx->pos == '!' && - strncmp(ctx->pos + 1, "[CDATA[", 7) == 0) { - /* CDATA */ - ctx->pos += 8; /* Skip '![CDATA[' */ - size_t len; - const char *cdata = parse_cdata(ctx, &len); - if (!cdata) { - taurus_element_free_tree(elem); - return NULL; - } - /* Add CDATA as text content */ - add_text_to_element(elem, cdata, len); - } else { - /* Child element - already positioned at element name after '<' */ - struct taurus_element *child = parse_element(ctx, elem); - if (!child) { - taurus_element_free_tree(elem); - return NULL; - } - /* child already added to elem by parse_start_tag */ - } - } else { - /* Text content */ - size_t len; - const char *text = parse_text(ctx, &len); - if (text) { - add_text_to_element(elem, text, len); - } - } - } - - /* Parse end tag */ - if (parse_end_tag(ctx, elem->name) < 0) { - taurus_element_free_tree(elem); - return NULL; - } - - return elem; -} \ No newline at end of file diff --git a/ext/taurus/lib/src/parse_helpers.c b/ext/taurus/lib/src/parse_helpers.c deleted file mode 100644 index 4d5fe67..0000000 --- a/ext/taurus/lib/src/parse_helpers.c +++ /dev/null @@ -1,141 +0,0 @@ -/* parse_helpers.c - Implementation of parse helper functions - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - */ - -#include "parse_helpers.h" -#include - -/* ================================================================== - * STRING INTERNING IMPLEMENTATION - * ================================================================= */ - -/* Pre-interned strings for common attributes - defined as string literals */ -const char *g_intern_id = "id"; -const char *g_intern_class = "class"; -const char *g_intern_type = "type"; -const char *g_intern_name = "name"; -const char *g_intern_href = "href"; -const char *g_intern_src = "src"; -const char *g_intern_xmlns = "xmlns"; -const char *g_intern_style = "style"; -const char *g_intern_value = "value"; -const char *g_intern_lang = "lang"; - -/* Simple hash function - DJB2 */ -static size_t hash_string(const char *str, size_t len) { - size_t hash = 5381; - for (size_t i = 0; i < len; i++) { - hash = ((hash << 5) + hash) + (unsigned char)str[i]; - } - return hash; -} - -/* Initialize string interning table */ -void string_intern_table_init(StringInternTable *table) { - memset(table->entries, 0, sizeof(table->entries)); - table->initialized = 1; -} - -/* Fast-path for common attributes (bypasses hash table) - * Returns interned string directly for top 10 attributes, otherwise NULL */ -const char* string_intern_get_fast(const char *name, size_t len) { - /* Quick length-based dispatch */ - switch (len) { - case 2: - if (name[0] == 'i' && name[1] == 'd') return g_intern_id; - break; - case 3: - if (name[0] == 's' && name[1] == 'r' && name[2] == 'c') - return g_intern_src; - break; - case 4: - if (memcmp(name, "type", 4) == 0) return g_intern_type; - if (memcmp(name, "name", 4) == 0) return g_intern_name; - if (memcmp(name, "href", 4) == 0) return g_intern_href; - if (memcmp(name, "lang", 4) == 0) return g_intern_lang; - break; - case 5: - if (memcmp(name, "class", 5) == 0) return g_intern_class; - if (memcmp(name, "style", 5) == 0) return g_intern_style; - if (memcmp(name, "value", 5) == 0) return g_intern_value; - if (memcmp(name, "xmlns", 5) == 0) return g_intern_xmlns; - break; - } - return NULL; -} - -/* Get or create interned string - * Uses linear probing hash table for collision resolution */ -const char* string_intern_get(StringInternTable *table, const char *str, size_t len) { - /* Try fast path first (common attributes) */ - const char *fast = string_intern_get_fast(str, len); - if (fast != NULL) { - return fast; - } - - /* Compute hash */ - size_t hash = hash_string(str, len); - size_t index = hash & (STRING_INTERN_TABLE_SIZE - 1); /* Modulo by power of 2 */ - - /* Linear probing */ - for (size_t i = 0; i < STRING_INTERN_TABLE_SIZE; i++) { - size_t probe = (index + i) & (STRING_INTERN_TABLE_SIZE - 1); - StringInternEntry *entry = &table->entries[probe]; - - /* Empty slot - insert new entry */ - if (entry->key == NULL) { - /* Allocate and copy string */ - char *copy = (char*)taurus_malloc(len + 1); - memcpy(copy, str, len); - copy[len] = '\0'; - - entry->key = copy; - entry->hash = hash; - return copy; - } - - /* Existing entry - check if it matches */ - if (entry->hash == hash && strlen(entry->key) == len) { - if (memcmp(entry->key, str, len) == 0) { - return entry->key; /* Found existing interned string */ - } - } - } - - /* Table is full - fall back to allocating new string - * (This should be rare with 128 slots) */ - char *copy = (char*)taurus_malloc(len + 1); - memcpy(copy, str, len); - copy[len] = '\0'; - return copy; -} - -/* Clear the interning table (for testing/benchmarking) */ -void string_intern_table_clear(StringInternTable *table) { - for (size_t i = 0; i < STRING_INTERN_TABLE_SIZE; i++) { - if (table->entries[i].key != NULL) { - /* Don't free pre-interned strings (they're string literals) */ - if (table->entries[i].key != g_intern_id && - table->entries[i].key != g_intern_class && - table->entries[i].key != g_intern_type && - table->entries[i].key != g_intern_name && - table->entries[i].key != g_intern_href && - table->entries[i].key != g_intern_src && - table->entries[i].key != g_intern_xmlns && - table->entries[i].key != g_intern_style && - table->entries[i].key != g_intern_value && - table->entries[i].key != g_intern_lang) { - taurus_free((void*)table->entries[i].key); - } - table->entries[i].key = NULL; - table->entries[i].hash = 0; - } - } -} - -/* Free all interned strings */ -void string_intern_table_free(StringInternTable *table) { - string_intern_table_clear(table); - table->initialized = 0; -} \ No newline at end of file diff --git a/ext/taurus/lib/src/parse_helpers.h b/ext/taurus/lib/src/parse_helpers.h deleted file mode 100644 index 2197074..0000000 --- a/ext/taurus/lib/src/parse_helpers.h +++ /dev/null @@ -1,259 +0,0 @@ -/* parse_helpers.h - Helper functions and structures for XML parsing - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * Consolidates parsing utilities extracted from ext/taurus helper modules: - * - Character classification and scanning (from parse_inline.h) - * - Attribute stack (from attr_stack.h, converted to pure C) - * - String interning (from symbol_cache.h, converted to pure C) - * - Parse structures (from parse_structures.h, converted to pure C) - */ - -#ifndef TAURUS_PARSE_HELPERS_H -#define TAURUS_PARSE_HELPERS_H - -#include "taurus_internal.h" -#include "taurus_memory.h" -#include "chartype.h" -#include "simd_helpers.h" - -/* ================================================================== - * CHARACTER CLASSIFICATION AND SCANNING - * ================================================================= - * Inline hot-path functions for parser (from parse_inline.h) - */ - -/* Check if character is whitespace - uses fast table lookup */ -static inline int taurus_is_whitespace(char c) { - return is_whitespace_fast(c); -} - -/* Skip whitespace using SIMD vectorized scanning */ -static inline void taurus_skip_whitespace(const char **pos, const char *end) { - *pos = simd_skip_whitespace(*pos, end); -} - -/* Check if character is valid for XML names - uses fast table lookup */ -static inline int taurus_is_name_char(char c) { - return is_name_char_fast(c); -} - -/* Peek at current character without advancing */ -static inline char taurus_peek_char(const char *pos, const char *end) { - if (pos < end) { - return *pos; - } - return '\0'; -} - -/* Get current character and advance position */ -static inline char taurus_next_char(const char **pos, const char *end) { - if (*pos < end) { - return *(*pos)++; - } - return '\0'; -} - -/* ================================================================== - * ATTRIBUTE STACK - * ================================================================= - * Stack-based attribute storage with inline base array - * Converted from ext/taurus/attr_stack.h (Ruby memory → pure C) - */ - -#define TAURUS_ATTR_STACK_BASE_SIZE 8 - -/* Attribute structure for parsing (distinct from final taurus_attribute) */ -typedef struct parse_attribute { - const char *name; /* Attribute name (pointer into parse buffer) */ - const char *value; /* Attribute value (pointer into parse buffer) */ -} ParseAttribute; - -/* Attribute stack with inline base array to avoid heap allocation for small elements */ -typedef struct taurus_attr_stack { - ParseAttribute base[TAURUS_ATTR_STACK_BASE_SIZE]; /* 8 inline slots */ - ParseAttribute *head; /* Start of allocated space */ - ParseAttribute *end; /* End of allocated space */ - ParseAttribute *tail; /* Current position (one past last element) */ -} TaurusAttrStack; - -/* Initialize attribute stack */ -static inline void taurus_attr_stack_init(TaurusAttrStack *stack) { - stack->head = stack->base; - stack->end = stack->base + TAURUS_ATTR_STACK_BASE_SIZE; - stack->tail = stack->head; -} - -/* Cleanup attribute stack (only needed if heap was allocated) */ -static inline void taurus_attr_stack_cleanup(TaurusAttrStack *stack) { - if (stack->base != stack->head) { - /* Heap memory was allocated, free it */ - taurus_free(stack->head); - stack->head = stack->base; - } -} - -/* Push attribute onto stack, growing if necessary */ -static inline void taurus_attr_stack_push(TaurusAttrStack *stack, - const char *name, - const char *value) { - /* Check if we need to grow */ - if (stack->end <= stack->tail + 1) { - size_t len = stack->end - stack->head; - size_t toff = stack->tail - stack->head; - - if (stack->base == stack->head) { - /* First time exceeding base array - allocate on heap */ - stack->head = (ParseAttribute*)taurus_malloc( - sizeof(ParseAttribute) * (len + TAURUS_ATTR_STACK_BASE_SIZE) - ); - memcpy(stack->head, stack->base, sizeof(ParseAttribute) * len); - } else { - /* Already on heap - reallocate */ - stack->head = (ParseAttribute*)taurus_realloc( - stack->head, - sizeof(ParseAttribute) * (len + TAURUS_ATTR_STACK_BASE_SIZE) - ); - } - stack->tail = stack->head + toff; - stack->end = stack->head + len + TAURUS_ATTR_STACK_BASE_SIZE; - } - - /* Add attribute */ - stack->tail->name = name; - stack->tail->value = value; - stack->tail++; -} - -/* Get number of attributes in stack */ -static inline size_t taurus_attr_stack_size(const TaurusAttrStack *stack) { - return stack->tail - stack->head; -} - -/* Get pointer to attribute array */ -static inline ParseAttribute *taurus_attr_stack_to_array(TaurusAttrStack *stack) { - return stack->head; -} - -/* Get attribute at index */ -static inline ParseAttribute *taurus_attr_stack_at(TaurusAttrStack *stack, size_t index) { - if (index < taurus_attr_stack_size(stack)) { - return &stack->head[index]; - } - return NULL; -} - -/* ================================================================== - * STRING INTERNING - * ================================================================= - * String interning table for attribute names - * Replaces symbol_cache.h (Ruby symbols → interned strings) - * - * PURPOSE: Eliminate duplicate string allocations. Multiple attributes - * with the same name (e.g., "id") share a single interned string. - * Compare by pointer equality instead of strcmp(). - */ - -#define STRING_INTERN_TABLE_SIZE 128 /* Power of 2 for fast modulo */ - -typedef struct string_intern_entry { - char *key; /* Interned string (owned copy) */ - size_t hash; /* Cached hash value */ -} StringInternEntry; - -typedef struct string_intern_table { - StringInternEntry entries[STRING_INTERN_TABLE_SIZE]; - int initialized; -} StringInternTable; - -/* Pre-interned strings for top 10 most common attributes (HTML/XML) - * Avoids hash table lookup for 80% of attribute accesses */ -extern const char *g_intern_id; -extern const char *g_intern_class; -extern const char *g_intern_type; -extern const char *g_intern_name; -extern const char *g_intern_href; -extern const char *g_intern_src; -extern const char *g_intern_xmlns; -extern const char *g_intern_style; -extern const char *g_intern_value; -extern const char *g_intern_lang; - -/* Initialize string interning table */ -void string_intern_table_init(StringInternTable *table); - -/* Get or create interned string - * Returns pointer to interned string (owned by table) - * Same string always returns same pointer (pointer equality) */ -const char* string_intern_get(StringInternTable *table, const char *str, size_t len); - -/* Fast-path for common attributes (bypasses hash table) - * Returns interned string directly for top 10 attributes, otherwise NULL */ -const char* string_intern_get_fast(const char *name, size_t len); - -/* Clear the interning table (for testing/benchmarking) */ -void string_intern_table_clear(StringInternTable *table); - -/* Free all interned strings */ -void string_intern_table_free(StringInternTable *table); - -/* ================================================================== - * PARSE STRUCTURES - * ================================================================= - * Temporary structures during parsing, converted from parse_structures.h - * (VALUE → struct taurus_element*) - */ - -/* Parsed element structure - pure C, no Ruby objects - * This structure holds parsed element data temporarily - * before taurus_element structures are created */ -typedef struct parsed_element { - char *name; /* Element name (pointer into buffer) */ - char *prefix; /* Namespace prefix (pointer into buffer) */ - TaurusAttrStack *attrs; /* Attributes (stack-based) */ - int has_children; /* Flag for whether element has child nodes */ - int is_self_closing; /* Flag for self-closing elements */ -} ParsedElement; - -/* Initialize a ParsedElement structure */ -static inline void parsed_element_init(ParsedElement *elem, TaurusAttrStack *attr_stack) { - elem->name = NULL; - elem->prefix = NULL; - elem->attrs = attr_stack; - elem->has_children = 0; - elem->is_self_closing = 0; -} - -/* Parser callbacks - builds C DOM structures - * These callbacks are invoked with parsed C data during parsing. - * The callback implementation creates taurus_element structures from the C data. */ -typedef struct parse_callbacks { - /* Called when element start tag is parsed - * context: Parent element (or NULL for document root) - * elem: Parsed element data (C structure) - * Returns: Newly created element (for parent tracking) */ - struct taurus_element* (*start_element)(struct taurus_element *context, ParsedElement *elem); - - /* Called when element end tag is parsed - * context: Parent element - * name: Element name being closed */ - void (*end_element)(struct taurus_element *context, const char *name); - - /* Called when text content is parsed - * context: Current element - * text: Text content (pointer into buffer) */ - void (*add_text)(struct taurus_element *context, const char *text); - - /* Called when comment is parsed - * context: Current element - * comment: Comment text (pointer into buffer) */ - void (*add_comment)(struct taurus_element *context, const char *comment); - - /* Called when CDATA is parsed - * context: Current element - * cdata: CDATA content (pointer into buffer) - * len: Length of CDATA */ - void (*add_cdata)(struct taurus_element *context, const char *cdata, size_t len); -} ParseCallbacks; - -#endif /* TAURUS_PARSE_HELPERS_H */ \ No newline at end of file diff --git a/ext/taurus/lib/src/parse_internal.h b/ext/taurus/lib/src/parse_internal.h deleted file mode 100644 index ec230d6..0000000 --- a/ext/taurus/lib/src/parse_internal.h +++ /dev/null @@ -1,77 +0,0 @@ -/* parse_internal.h - Internal parser function declarations - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * INTERNAL HEADER - Not part of public API - * Shared between parse modules (parse_document.c, parse_element.c, parse_content.c) - */ - -#ifndef TAURUS_PARSE_INTERNAL_H -#define TAURUS_PARSE_INTERNAL_H - -#include "taurus_parse.h" -#include "taurus_internal.h" -#include "parse_helpers.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/* ================================================================== - * STRING HELPERS (from taurus_parse.c) - * ================================================================== */ - -/* Duplicate string from buffer with length (NOT null-terminated in source) - * Caller must free returned string with taurus_free() */ -char *taurus_strndup(const char *str, size_t len); - -/* Extract prefix and local name from qualified name "prefix:local" - * Returns 0 if no prefix, 1 if prefix found, -1 on error - * Modifies qname buffer in place (replaces ':' with '\0') - * Caller must free qname after use */ -int extract_prefix_and_local(char *qname, char **prefix, char **local_name); - -/* Extract just local name from qualified name (for end tag comparison) - * Returns pointer into qname buffer (after ':' or whole name if no prefix) */ -const char *extract_local_name_only(const char *qname); - -/* ================================================================== - * CONTENT PROCESSING (from parse_content.c) - * ================================================================== */ - -/* Expand entity reference &name; or &#number; or &#xhex; - * Returns expanded string (MUST be freed by caller) or NULL if unknown */ -char* expand_entity_reference(const char* ref, size_t len); - -/* Add text content to element (creates or appends to text_content field) - * Automatically expands entity references during addition */ -void add_text_to_element(struct taurus_element *elem, const char *text, size_t len); - -/* Process namespace declarations from attribute stack - * Extracts xmlns and xmlns:prefix attributes and creates namespace objects */ -void process_namespace_declarations(struct taurus_element *elem, TaurusAttrStack *attr_stack); - -/* Set element namespace from prefix or parent inheritance - * Resolves prefix to URI using namespace chain */ -void set_element_namespace(struct taurus_element *elem, const char *prefix, - struct taurus_element *parent); - -/* Add non-xmlns attributes to element - * Filters out namespace declarations and creates attribute objects */ -void add_attributes_to_element(struct taurus_element *elem, TaurusAttrStack *attr_stack, - StringInternTable *intern_table); - -/* ================================================================== - * PROCESSING INSTRUCTION PARSING (from parse_document.c) - * ================================================================== */ - -/* Parse processing instruction - * Assumes positioned after ' - -/* Parser state for position tracking */ -typedef struct { - const char* input; /* Original input (for context extraction) */ - const char* pos; /* Current position in input */ - size_t offset; /* Byte offset from start */ - int line; /* Current line (1-based) */ - int column; /* Current column (1-based) */ -} ParserState; - -/* Helper: Advance one character and update position */ -static void advance_char(ParserState* state) { - if (!state || !state->pos || !*state->pos) return; - - if (*state->pos == '\n') { - state->line++; - state->column = 1; - } else { - state->column++; - } - state->pos++; - state->offset++; -} - -/* Helper: Skip whitespace and update position */ -static void skip_whitespace(ParserState* state) { - while (state->pos && *state->pos && isspace((unsigned char)*state->pos)) { - advance_char(state); - } -} - -/* Helper: Parse element name */ -static char* parse_name(const char** p) { - const char* start = *p; - const char* end = start; - - /* Name: [a-zA-Z_][a-zA-Z0-9_.-]* */ - if (!isalpha((unsigned char)*end) && *end != '_') return NULL; - - end++; - while (isalnum((unsigned char)*end) || *end == '_' || *end == '-' || *end == '.' || *end == ':') { - end++; - } - - size_t len = end - start; - if (len == 0) return NULL; - - char* name = TAURUS_ALLOC_N(char, len + 1); - if (!name) return NULL; - - memcpy(name, start, len); - name[len] = '\0'; - *p = end; - - return name; -} - -/* Helper: Parse attribute value */ -static char* parse_attr_value(const char** p) { - const char* pos = *p; - - /* Skip whitespace */ - while (*pos && isspace((unsigned char)*pos)) pos++; - - /* Must have = */ - if (*pos != '=') return NULL; - pos++; - - /* Skip whitespace */ - while (*pos && isspace((unsigned char)*pos)) pos++; - - /* Must have quote */ - char quote = *pos; - if (quote != '"' && quote != '\'') return NULL; - pos++; - - /* Find closing quote */ - const char* start = pos; - while (*pos && *pos != quote) pos++; - - if (*pos != quote) return NULL; - - size_t len = pos - start; - char* value = TAURUS_ALLOC_N(char, len + 1); - if (!value) return NULL; - - memcpy(value, start, len); - value[len] = '\0'; - - pos++; /* Skip closing quote */ - *p = pos; - - return value; -} - -/* Helper: Parse attributes and process namespace declarations */ -static void parse_attributes(const char** p, struct taurus_element* elem) { - const char* pos = *p; - - while (*pos && *pos != '>' && *pos != '/') { - /* Skip whitespace */ - while (*pos && isspace((unsigned char)*pos)) pos++; - - if (*pos == '>' || *pos == '/') break; - - /* Parse attribute name */ - char* name = parse_name(&pos); - if (!name) break; - - /* Parse attribute value */ - char* value = parse_attr_value(&pos); - if (!value) { - TAURUS_FREE(name); - break; - } - - /* Check if this is a namespace declaration */ - if (strcmp(name, "xmlns") == 0) { - /* Default namespace: xmlns="uri" */ - struct taurus_namespace* ns = TAURUS_ALLOC(struct taurus_namespace); - if (ns) { - ns->prefix = NULL; - ns->uri = taurus_strdup(value); - ns->next = elem->namespaces; - elem->namespaces = ns; - elem->namespaces_count++; - } - TAURUS_FREE(name); - TAURUS_FREE(value); - continue; - } else if (strncmp(name, "xmlns:", 6) == 0) { - /* Prefixed namespace: xmlns:prefix="uri" */ - const char* prefix = name + 6; - struct taurus_namespace* ns = TAURUS_ALLOC(struct taurus_namespace); - if (ns) { - ns->prefix = taurus_strdup(prefix); - ns->uri = taurus_strdup(value); - ns->next = elem->namespaces; - elem->namespaces = ns; - elem->namespaces_count++; - } - TAURUS_FREE(name); - TAURUS_FREE(value); - continue; - } - - /* Regular attribute */ - struct taurus_attribute* attr = TAURUS_ALLOC(struct taurus_attribute); - if (!attr) { - TAURUS_FREE(name); - TAURUS_FREE(value); - break; - } - - attr->name = name; - attr->value = value; - attr->prefix = NULL; - attr->namespace_uri = NULL; - - /* Add to element's attributes array */ - if (elem->attributes_count >= elem->attributes_capacity) { - size_t new_cap = elem->attributes_capacity == 0 ? 4 : elem->attributes_capacity * 2; - struct taurus_attribute** new_attrs = TAURUS_REALLOC_N( - elem->attributes, struct taurus_attribute*, new_cap); - if (!new_attrs) { - TAURUS_FREE(attr->name); - TAURUS_FREE(attr->value); - TAURUS_FREE(attr); - break; - } - elem->attributes = new_attrs; - elem->attributes_capacity = new_cap; - } - - elem->attributes[elem->attributes_count++] = attr; - } - - *p = pos; -} - -/* Helper: Resolve namespace URI for element */ -static void resolve_element_namespace(struct taurus_element* elem) { - if (!elem) return; - - /* Extract prefix from element name if present */ - const char* colon = strchr(elem->name, ':'); - char* prefix = NULL; - - if (colon) { - size_t prefix_len = colon - elem->name; - prefix = TAURUS_ALLOC_N(char, prefix_len + 1); - if (prefix) { - memcpy(prefix, elem->name, prefix_len); - prefix[prefix_len] = '\0'; - elem->prefix = prefix; - } - } - - /* Search for matching namespace declaration */ - struct taurus_element* current = elem; - while (current) { - struct taurus_namespace* ns = current->namespaces; - while (ns) { - /* Check if this namespace matches our prefix */ - if (!prefix && !ns->prefix) { - /* Default namespace match */ - elem->namespace_uri = taurus_strdup(ns->uri); - return; - } - if (prefix && ns->prefix && strcmp(prefix, ns->prefix) == 0) { - /* Prefixed namespace match */ - elem->namespace_uri = taurus_strdup(ns->uri); - return; - } - ns = ns->next; - } - current = current->parent; - } -} - -/* Helper: Resolve namespaces recursively for element and all descendants */ -static void resolve_namespaces_recursive(struct taurus_element* elem) { - if (!elem) return; - - /* Resolve this element's namespace */ - resolve_element_namespace(elem); - - /* Recursively resolve children */ - for (size_t i = 0; i < elem->children_count; i++) { - resolve_namespaces_recursive(elem->children[i]); - } -} - -/* Helper: Parse text content */ -static char* parse_text(const char** p) { - size_t capacity = 64; - size_t len = 0; - char* text = TAURUS_ALLOC_N(char, capacity); - if (!text) return NULL; - - while (**p && **p != '<') { - if (len + 1 >= capacity) { - capacity *= 2; - char* new_text = TAURUS_REALLOC_N(text, char, capacity); - if (!new_text) { - TAURUS_FREE(text); - return NULL; - } - text = new_text; - } - text[len++] = **p; - (*p)++; - } - - text[len] = '\0'; - - /* Trim whitespace */ - char* trimmed_start = text; - while (*trimmed_start && isspace((unsigned char)*trimmed_start)) { - trimmed_start++; - } - - if (*trimmed_start == '\0') { - TAURUS_FREE(text); - return NULL; - } - - char* result = taurus_strdup(trimmed_start); - TAURUS_FREE(text); - return result; -} - -/* Forward declaration */ -static struct taurus_element* parse_element(const char** p); - -/* Helper: Add child to element */ -static void add_child(struct taurus_element* parent, struct taurus_element* child) { - if (!parent || !child) return; - - if (parent->children_count >= parent->children_capacity) { - size_t new_cap = parent->children_capacity == 0 ? 4 : parent->children_capacity * 2; - struct taurus_element** new_children = TAURUS_REALLOC_N( - parent->children, struct taurus_element*, new_cap); - if (!new_children) return; - parent->children = new_children; - parent->children_capacity = new_cap; - } - - parent->children[parent->children_count++] = child; - child->parent = parent; -} - -/* Parse element: content */ -static struct taurus_element* parse_element(const char** p) { - /* Skip whitespace manually */ - while (**p && isspace((unsigned char)**p)) (*p)++; - - const char* pos = *p; - - /* Must start with < */ - if (*pos != '<') return NULL; - pos++; - - /* Parse tag name */ - char* name = parse_name(&pos); - if (!name) return NULL; - - /* Create element */ - struct taurus_element* elem = TAURUS_ALLOC(struct taurus_element); - if (!elem) { - TAURUS_FREE(name); - return NULL; - } - memset(elem, 0, sizeof(struct taurus_element)); - elem->name = name; - elem->doc_order = -1; - - /* Parse attributes (includes namespace declarations) */ - parse_attributes(&pos, elem); - - /* Self-closing tag? */ - if (*pos == '/') { - pos++; - if (*pos == '>') pos++; - *p = pos; - return elem; - } - if (*pos == '>') pos++; - - /* Parse content */ - while (*pos) { - /* Skip whitespace manually */ - while (*pos && isspace((unsigned char)*pos)) pos++; - - if (*pos == '<') { - if (*(pos + 1) == '/') { - /* Closing tag */ - pos += 2; - char* close_name = parse_name(&pos); - if (close_name) { - TAURUS_FREE(close_name); - } - /* Skip whitespace manually */ - while (*pos && isspace((unsigned char)*pos)) pos++; - if (*pos == '>') pos++; - *p = pos; - return elem; - } else { - /* Child element */ - struct taurus_element* child = parse_element(&pos); - if (child) { - add_child(elem, child); - /* Note: Namespace resolution now done recursively after full tree built */ - } - } - } else { - /* Text content */ - char* text = parse_text(&pos); - if (text) { - elem->text_content = text; - } - } - } - - *p = pos; - return elem; -} - -/* Parse XML document with position tracking */ -struct taurus_document* parse_xml_simple(const char* xml, size_t len) { - /* Validate input */ - if (!xml) { - taurus_set_error(TAURUS_ERROR_NULL_INPUT, "NULL input provided"); - return NULL; - } - - if (len == 0) { - taurus_set_error(TAURUS_ERROR_EMPTY_INPUT, "Empty input provided"); - return NULL; - } - - /* Initialize parser state */ - ParserState state; - state.input = xml; - state.pos = xml; - state.offset = 0; - state.line = 1; - state.column = 1; - - /* Create document */ - struct taurus_document* doc = TAURUS_ALLOC(struct taurus_document); - if (!doc) { - taurus_set_error(TAURUS_ERROR_OUT_OF_MEMORY, "Failed to allocate document"); - return NULL; - } - - memset(doc, 0, sizeof(struct taurus_document)); - doc->ref_count = 1; - - /* Skip XML declaration and processing instructions */ - skip_whitespace(&state); - - /* Skip declaration and other PIs */ - while (*state.pos == '<' && *(state.pos + 1) == '?') { - /* Find closing ?> */ - advance_char(&state); /* < */ - advance_char(&state); /* ? */ - while (*state.pos && !(*state.pos == '?' && *(state.pos + 1) == '>')) { - advance_char(&state); - } - if (*state.pos == '?' && *(state.pos + 1) == '>') { - advance_char(&state); /* ? */ - advance_char(&state); /* > */ - } - skip_whitespace(&state); - } - - /* Skip comments */ - while (*state.pos == '<' && *(state.pos + 1) == '!' && - *(state.pos + 2) == '-' && *(state.pos + 3) == '-') { - /* Find closing --> */ - advance_char(&state); /* < */ - advance_char(&state); /* ! */ - advance_char(&state); /* - */ - advance_char(&state); /* - */ - while (*state.pos && !(*state.pos == '-' && *(state.pos + 1) == '-' && *(state.pos + 2) == '>')) { - advance_char(&state); - } - if (*state.pos == '-' && *(state.pos + 1) == '-' && *(state.pos + 2) == '>') { - advance_char(&state); /* - */ - advance_char(&state); /* - */ - advance_char(&state); /* > */ - } - skip_whitespace(&state); - } - - /* Check for root element */ - if (*state.pos != '<') { - taurus_set_error_with_context( - TAURUS_ERROR_INVALID_XML, - "Expected root element", - state.input, - state.offset, - state.line, - state.column - ); - TAURUS_FREE(doc); - return NULL; - } - - /* Parse root element (use old interface for now) */ - const char* pos = state.pos; - doc->root = parse_element(&pos); - - if (!doc->root) { - /* Get current position after failed parse */ - size_t failed_offset = pos - xml; - int failed_line = state.line; - int failed_col = state.column; - - /* Calculate actual line/column if parse advanced */ - const char* p = state.pos; - while (p < pos) { - if (*p == '\n') { - failed_line++; - failed_col = 1; - } else { - failed_col++; - } - p++; - } - - taurus_set_error_with_context( - TAURUS_ERROR_PARSE_FAILED, - "Failed to parse root element", - state.input, - failed_offset, - failed_line, - failed_col - ); - TAURUS_FREE(doc); - return NULL; - } - - /* Resolve namespaces for entire tree after parsing complete - * This ensures all elements have correct namespace_uri */ - resolve_namespaces_recursive(doc->root); - - return doc; -} - -/* Free element recursively */ -void free_element(struct taurus_element* elem) { - if (!elem) return; - - /* Free children */ - for (size_t i = 0; i < elem->children_count; i++) { - free_element(elem->children[i]); - } - - /* Free attributes and their content */ - for (size_t i = 0; i < elem->attributes_count; i++) { - if (elem->attributes[i]) { - if (elem->attributes[i]->name) TAURUS_FREE(elem->attributes[i]->name); - if (elem->attributes[i]->value) TAURUS_FREE(elem->attributes[i]->value); - if (elem->attributes[i]->prefix) TAURUS_FREE(elem->attributes[i]->prefix); - if (elem->attributes[i]->namespace_uri) TAURUS_FREE(elem->attributes[i]->namespace_uri); - TAURUS_FREE(elem->attributes[i]); - } - } - - /* Free arrays */ - if (elem->children) TAURUS_FREE(elem->children); - if (elem->attributes) TAURUS_FREE(elem->attributes); - - /* Free strings */ - if (elem->name) TAURUS_FREE(elem->name); - if (elem->prefix) TAURUS_FREE(elem->prefix); - if (elem->namespace_uri) TAURUS_FREE(elem->namespace_uri); - if (elem->text_content) TAURUS_FREE(elem->text_content); - - /* Free element */ - TAURUS_FREE(elem); -} \ No newline at end of file diff --git a/ext/taurus/lib/src/simd_helpers.h b/ext/taurus/lib/src/simd_helpers.h deleted file mode 100644 index ef31e1a..0000000 --- a/ext/taurus/lib/src/simd_helpers.h +++ /dev/null @@ -1,445 +0,0 @@ -/* simd_helpers.h - SIMD acceleration for Taurus parser - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * Platform-agnostic SIMD operations: - * - x86_64: SSE2 intrinsics (128-bit vectors, 16 bytes at once) - * - ARM64: NEON intrinsics (128-bit vectors, 16 bytes at once) - * - Fallback: Scalar operations for other platforms - * - * PERFORMANCE TARGET: 3-5× speedup on hot-path operations - */ - -#ifndef TAURUS_SIMD_HELPERS_H -#define TAURUS_SIMD_HELPERS_H - -#include -#include -#include - -/* Detect platform and include appropriate SIMD headers */ -#if defined(__x86_64__) || defined(_M_X64) - #define TAURUS_SIMD_SSE2 1 - #include /* SSE2 intrinsics */ - typedef __m128i simd_vec_t; -#elif defined(__aarch64__) || defined(_M_ARM64) - #define TAURUS_SIMD_NEON 1 - #include /* NEON intrinsics */ - typedef uint8x16_t simd_vec_t; -#else - #define TAURUS_SIMD_NONE 1 -#endif - -/* SIMD vector size (16 bytes for both SSE2 and NEON) */ -#define SIMD_VEC_SIZE 16 - -/* ================================================================== - * SIMD WHITESPACE SCANNING - * ================================================================= - * Scans for whitespace (space, tab, newline, carriage return) using - * SIMD comparison operations. Processes 16 bytes per iteration. - */ - -inline static const char* simd_skip_whitespace(const char* pos, const char* end) { - const char* p = pos; - -#if defined(TAURUS_SIMD_SSE2) - /* SSE2 path - x86_64 */ - __m128i space = _mm_set1_epi8(' '); - __m128i tab = _mm_set1_epi8('\t'); - __m128i newline = _mm_set1_epi8('\n'); - __m128i carriage = _mm_set1_epi8('\r'); - - /* Process 16 bytes at a time */ - while (p + SIMD_VEC_SIZE <= end) { - __m128i chunk = _mm_loadu_si128((__m128i*)p); - - /* Compare with all whitespace chars */ - __m128i is_space = _mm_cmpeq_epi8(chunk, space); - __m128i is_tab = _mm_cmpeq_epi8(chunk, tab); - __m128i is_newline = _mm_cmpeq_epi8(chunk, newline); - __m128i is_carriage = _mm_cmpeq_epi8(chunk, carriage); - - /* Combine all whitespace matches */ - __m128i is_ws = _mm_or_si128( - _mm_or_si128(is_space, is_tab), - _mm_or_si128(is_newline, is_carriage) - ); - - /* Find first non-whitespace */ - int mask = _mm_movemask_epi8(is_ws); - if (mask != 0xFFFF) { /* Found non-whitespace */ - /* Count leading whitespace bytes */ - int leading = __builtin_ctz(~mask & 0xFFFF); - return p + leading; - } - - p += SIMD_VEC_SIZE; - } - -#elif defined(TAURUS_SIMD_NEON) - /* NEON path - ARM64 */ - uint8x16_t space = vdupq_n_u8(' '); - uint8x16_t tab = vdupq_n_u8('\t'); - uint8x16_t newline = vdupq_n_u8('\n'); - uint8x16_t carriage = vdupq_n_u8('\r'); - - /* Process 16 bytes at a time */ - while (p + SIMD_VEC_SIZE <= end) { - uint8x16_t chunk = vld1q_u8((uint8_t*)p); - - /* Compare with all whitespace chars */ - uint8x16_t is_space = vceqq_u8(chunk, space); - uint8x16_t is_tab = vceqq_u8(chunk, tab); - uint8x16_t is_newline = vceqq_u8(chunk, newline); - uint8x16_t is_carriage = vceqq_u8(chunk, carriage); - - /* Combine all whitespace matches */ - uint8x16_t is_ws = vorrq_u8( - vorrq_u8(is_space, is_tab), - vorrq_u8(is_newline, is_carriage) - ); - - /* Check if we have any non-whitespace */ - uint64_t mask = vget_lane_u64(vreinterpret_u64_u8(vmovn_u16( - vreinterpretq_u16_u8(is_ws))), 0); - - if (mask != 0xFFFFFFFFFFFFFFFFULL) { /* Found non-whitespace */ - /* Fall back to scalar for this chunk */ - while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { - p++; - } - return p; - } - - p += SIMD_VEC_SIZE; - } -#endif - - /* Scalar fallback for remaining bytes (< 16) or unsupported platforms */ - while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { - p++; - } - - return p; -} - -/* ================================================================== - * SIMD NAME CHARACTER CLASSIFICATION - * ================================================================= - * Validates XML name characters: a-z, A-Z, 0-9, _, -, ., : - * Uses SIMD range checks for massive speedup. - */ - -inline static int simd_is_name_char(char c) { - return (c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || - c == '_' || c == '-' || c == '.' || c == ':'; -} - -/* Scan for end of name using SIMD */ -inline static const char* simd_scan_name(const char* start, const char* end) { - const char* p = start; - -#if defined(TAURUS_SIMD_SSE2) - /* SSE2 path - Check ranges in parallel */ - __m128i lower_a = _mm_set1_epi8('a' - 1); - __m128i upper_z = _mm_set1_epi8('z' + 1); - __m128i lower_A = _mm_set1_epi8('A' - 1); - __m128i upper_Z = _mm_set1_epi8('Z' + 1); - __m128i lower_0 = _mm_set1_epi8('0' - 1); - __m128i upper_9 = _mm_set1_epi8('9' + 1); - __m128i underscore = _mm_set1_epi8('_'); - __m128i dash = _mm_set1_epi8('-'); - __m128i dot = _mm_set1_epi8('.'); - __m128i colon = _mm_set1_epi8(':'); - - while (p + SIMD_VEC_SIZE <= end) { - __m128i chunk = _mm_loadu_si128((__m128i*)p); - - /* Check ranges: a-z, A-Z, 0-9 */ - __m128i is_lower = _mm_and_si128( - _mm_cmpgt_epi8(chunk, lower_a), - _mm_cmplt_epi8(chunk, upper_z) - ); - __m128i is_upper = _mm_and_si128( - _mm_cmpgt_epi8(chunk, lower_A), - _mm_cmplt_epi8(chunk, upper_Z) - ); - __m128i is_digit = _mm_and_si128( - _mm_cmpgt_epi8(chunk, lower_0), - _mm_cmplt_epi8(chunk, upper_9) - ); - - /* Check special chars: _, -, ., : */ - __m128i is_underscore = _mm_cmpeq_epi8(chunk, underscore); - __m128i is_dash = _mm_cmpeq_epi8(chunk, dash); - __m128i is_dot = _mm_cmpeq_epi8(chunk, dot); - __m128i is_colon = _mm_cmpeq_epi8(chunk, colon); - - /* Combine all valid character checks */ - __m128i is_valid = _mm_or_si128( - _mm_or_si128( - _mm_or_si128(is_lower, is_upper), - _mm_or_si128(is_digit, is_underscore) - ), - _mm_or_si128( - _mm_or_si128(is_dash, is_dot), - is_colon - ) - ); - - /* Find first invalid character */ - int mask = _mm_movemask_epi8(is_valid); - if (mask != 0xFFFF) { /* Found invalid char */ - int valid_count = __builtin_ctz(~mask & 0xFFFF); - return p + valid_count; - } - - p += SIMD_VEC_SIZE; - } - -#elif defined(TAURUS_SIMD_NEON) - /* NEON path - ARM64 */ - uint8x16_t lower_a = vdupq_n_u8('a' - 1); - uint8x16_t upper_z = vdupq_n_u8('z'); - uint8x16_t lower_A = vdupq_n_u8('A' - 1); - uint8x16_t upper_Z = vdupq_n_u8('Z'); - uint8x16_t lower_0 = vdupq_n_u8('0' - 1); - uint8x16_t upper_9 = vdupq_n_u8('9'); - uint8x16_t underscore = vdupq_n_u8('_'); - uint8x16_t dash = vdupq_n_u8('-'); - uint8x16_t dot = vdupq_n_u8('.'); - uint8x16_t colon = vdupq_n_u8(':'); - - while (p + SIMD_VEC_SIZE <= end) { - uint8x16_t chunk = vld1q_u8((uint8_t*)p); - - /* Check ranges */ - uint8x16_t is_lower = vandq_u8( - vcgtq_u8(chunk, lower_a), - vcleq_u8(chunk, upper_z) - ); - uint8x16_t is_upper = vandq_u8( - vcgtq_u8(chunk, lower_A), - vcleq_u8(chunk, upper_Z) - ); - uint8x16_t is_digit = vandq_u8( - vcgtq_u8(chunk, lower_0), - vcleq_u8(chunk, upper_9) - ); - - /* Check special chars */ - uint8x16_t is_underscore = vceqq_u8(chunk, underscore); - uint8x16_t is_dash = vceqq_u8(chunk, dash); - uint8x16_t is_dot = vceqq_u8(chunk, dot); - uint8x16_t is_colon = vceqq_u8(chunk, colon); - - /* Combine all checks */ - uint8x16_t is_valid = vorrq_u8( - vorrq_u8( - vorrq_u8(is_lower, is_upper), - vorrq_u8(is_digit, is_underscore) - ), - vorrq_u8( - vorrq_u8(is_dash, is_dot), - is_colon - ) - ); - - /* Check if all bytes are valid name chars */ - uint64_t low = vgetq_lane_u64(vreinterpretq_u64_u8(is_valid), 0); - uint64_t high = vgetq_lane_u64(vreinterpretq_u64_u8(is_valid), 1); - - if (~(low & high)) { /* Found invalid char */ - /* Fall back to scalar for exact position */ - break; - } - - p += SIMD_VEC_SIZE; - } -#endif - - /* Scalar finish for remaining bytes or on non-SIMD platforms */ - while (p < end && simd_is_name_char(*p)) { - p++; - } - - return p; -} - -/* ================================================================== - * SIMD STRING COMPARISON - * ================================================================= - * Fast memcmp using SIMD for common short string comparisons. - * Optimized for typical XML name/namespace lookups (4-20 chars). - */ - -inline static int simd_memcmp(const char* s1, const char* s2, size_t n) { - /* For very short strings, scalar is faster (branch prediction) */ - if (n < 8) { - return memcmp(s1, s2, n); - } - -#if defined(TAURUS_SIMD_SSE2) - /* SSE2 path */ - const char *p1 = s1, *p2 = s2; - size_t remaining = n; - - while (remaining >= SIMD_VEC_SIZE) { - __m128i v1 = _mm_loadu_si128((__m128i*)p1); - __m128i v2 = _mm_loadu_si128((__m128i*)p2); - __m128i cmp = _mm_cmpeq_epi8(v1, v2); - - int mask = _mm_movemask_epi8(cmp); - if (mask != 0xFFFF) { /* Found difference */ - /* Find first differing byte */ - int diff_pos = __builtin_ctz(~mask & 0xFFFF); - return (unsigned char)p1[diff_pos] - (unsigned char)p2[diff_pos]; - } - - p1 += SIMD_VEC_SIZE; - p2 += SIMD_VEC_SIZE; - remaining -= SIMD_VEC_SIZE; - } - - /* Handle remaining bytes */ - return memcmp(p1, p2, remaining); - -#elif defined(TAURUS_SIMD_NEON) - /* NEON path */ - const char *p1 = s1, *p2 = s2; - size_t remaining = n; - - while (remaining >= SIMD_VEC_SIZE) { - uint8x16_t v1 = vld1q_u8((uint8_t*)p1); - uint8x16_t v2 = vld1q_u8((uint8_t*)p2); - uint8x16_t cmp = vceqq_u8(v1, v2); - - /* Check if all bytes match */ - uint64_t low = vgetq_lane_u64(vreinterpretq_u64_u8(cmp), 0); - uint64_t high = vgetq_lane_u64(vreinterpretq_u64_u8(cmp), 1); - - if ((low & high) != 0xFFFFFFFFFFFFFFFFULL) { /* Found difference */ - return memcmp(p1, p2, SIMD_VEC_SIZE); - } - - p1 += SIMD_VEC_SIZE; - p2 += SIMD_VEC_SIZE; - remaining -= SIMD_VEC_SIZE; - } - - return memcmp(p1, p2, remaining); -#else - /* Scalar fallback */ - return memcmp(s1, s2, n); -#endif -} - -/* ================================================================== - * SIMD PATTERN MATCHING - * ================================================================= - * Fast pattern matching for "xmlns" prefix detection. - * Uses SIMD to check multiple bytes simultaneously. - */ - -inline static int simd_starts_with_xmlns(const char* s) { - /* Quick scalar check for common non-xmlns cases */ - if (s[0] != 'x') return 0; - -#if defined(TAURUS_SIMD_SSE2) || defined(TAURUS_SIMD_NEON) - /* Load first 8 bytes (includes "xmlns" and potential ":") */ - /* We know s[0] == 'x', so just check remaining "mlns" */ - if (s[1] == 'm' && s[2] == 'l' && s[3] == 'n' && s[4] == 's') { - /* Check for xmlns or xmlns: */ - return (s[5] == '\0' || s[5] == ':'); - } - return 0; -#else - /* Scalar path */ - return (s[0] == 'x' && s[1] == 'm' && s[2] == 'l' && - s[3] == 'n' && s[4] == 's' && - (s[5] == '\0' || s[5] == ':')); -#endif -} - -/* ================================================================== - * SIMD STRING EQUALITY - * ================================================================= - * Fast string equality check for namespace prefix matching. - */ - -inline static int simd_streq(const char* s1, const char* s2, size_t len) { - /* For very short strings, direct comparison is faster */ - if (len <= 4) { - switch (len) { - case 0: return 1; - case 1: return s1[0] == s2[0]; - case 2: return s1[0] == s2[0] && s1[1] == s2[1]; - case 3: return s1[0] == s2[0] && s1[1] == s2[1] && s1[2] == s2[2]; - case 4: return *((uint32_t*)s1) == *((uint32_t*)s2); - } - } - - return simd_memcmp(s1, s2, len) == 0; -} - -/* ================================================================== - * SIMD FIRST CHARACTER SCANNING - * ================================================================= - * Find first occurrence of character using SIMD. - * Faster than strchr() for short strings. - */ - -inline static const char* simd_find_char(const char* s, const char* end, char target) { - const char* p = s; - -#if defined(TAURUS_SIMD_SSE2) - __m128i target_vec = _mm_set1_epi8(target); - - while (p + SIMD_VEC_SIZE <= end) { - __m128i chunk = _mm_loadu_si128((__m128i*)p); - __m128i cmp = _mm_cmpeq_epi8(chunk, target_vec); - - int mask = _mm_movemask_epi8(cmp); - if (mask != 0) { /* Found target */ - int pos = __builtin_ctz(mask); - return p + pos; - } - - p += SIMD_VEC_SIZE; - } - -#elif defined(TAURUS_SIMD_NEON) - uint8x16_t target_vec = vdupq_n_u8((uint8_t)target); - - while (p + SIMD_VEC_SIZE <= end) { - uint8x16_t chunk = vld1q_u8((uint8_t*)p); - uint8x16_t cmp = vceqq_u8(chunk, target_vec); - - /* Check if we found the target */ - uint64_t low = vgetq_lane_u64(vreinterpretq_u64_u8(cmp), 0); - uint64_t high = vgetq_lane_u64(vreinterpretq_u64_u8(cmp), 1); - - if (low || high) { /* Found target somewhere */ - /* Fall back to scalar for exact position */ - while (p < end && *p != target) { - p++; - } - return (p < end) ? p : NULL; - } - - p += SIMD_VEC_SIZE; - } -#endif - - /* Scalar finish */ - while (p < end && *p != target) { - p++; - } - - return (p < end) ? p : NULL; -} - -#endif /* TAURUS_SIMD_HELPERS_H */ \ No newline at end of file diff --git a/ext/taurus/lib/src/taurus.c b/ext/taurus/lib/src/taurus.c deleted file mode 100644 index 2cfa160..0000000 --- a/ext/taurus/lib/src/taurus.c +++ /dev/null @@ -1,222 +0,0 @@ -/* taurus.c - Taurus public API implementation - * Copyright (c) 2024, Ribose Inc. - * - * Pure C XML parser and XPath evaluator - Public API. - */ - -#include "taurus/taurus.h" -#include "taurus_internal.h" -#include "xpath/parser.h" -#include "xpath/evaluator.h" -#include - -/* Forward declaration from parse_simple.c */ -extern struct taurus_document* parse_xml_simple(const char* xml, size_t len); -extern void free_element(struct taurus_element* elem); - -/* ============================================================================ - * Version Information - * ============================================================================ */ - -/** - * Get library version string - */ -TAURUS_API const char* taurus_version(void) { - return TAURUS_VERSION; -} - -/** - * Get version components - */ -TAURUS_API void taurus_version_components(int* major, int* minor, int* patch) { - if (major) *major = TAURUS_VERSION_MAJOR; - if (minor) *minor = TAURUS_VERSION_MINOR; - if (patch) *patch = TAURUS_VERSION_PATCH; -} - -/* ============================================================================ - * Parse Options - * ============================================================================ */ - -/** - * Initialize parse options with defaults - */ -TAURUS_API void taurus_parse_options_init(taurus_parse_options* opts) { - if (!opts) return; - - opts->strict = 1; /* Strict mode by default */ - opts->preserve_whitespace = 0; /* Don't preserve whitespace by default */ - opts->track_positions = 0; /* Don't track positions by default */ -} - -/* ============================================================================ - * Document Functions - * ============================================================================ */ - -/** - * Parse XML string into document - */ -TAURUS_API struct taurus_document* taurus_parse(const char* xml, size_t len) { - if (!xml || len == 0) return NULL; - - /* Use default options */ - return parse_xml_simple(xml, len); -} - -/** - * Parse XML with custom options - */ -TAURUS_API struct taurus_document* taurus_parse_with_options( - const char* xml, - size_t len, - const taurus_parse_options* opts -) { - if (!xml || len == 0) return NULL; - - /* For now, ignore options and use simple parser - * TODO: Implement options support in parser */ - (void)opts; /* Suppress unused parameter warning */ - return parse_xml_simple(xml, len); -} - -/** - * Free document and all its contents - */ -TAURUS_API void taurus_document_free(struct taurus_document* doc) { - if (!doc) return; - - /* Decrement reference count */ - if (doc->ref_count > 0) { - doc->ref_count--; - if (doc->ref_count > 0) return; - } - - /* Free root element tree */ - if (doc->root) { - free_element(doc->root); - } - - /* Free document fields */ - if (doc->encoding) { - TAURUS_FREE(doc->encoding); - } - - /* Free processing instructions */ - struct taurus_processing_instruction* pi = doc->pis; - while (pi) { - struct taurus_processing_instruction* next = pi->next; - if (pi->target) TAURUS_FREE(pi->target); - if (pi->data) TAURUS_FREE(pi->data); - TAURUS_FREE(pi); - pi = next; - } - - /* Free document */ - TAURUS_FREE(doc); -} - -/** - * Get root element of document - */ -TAURUS_API struct taurus_element* taurus_document_root(struct taurus_document* doc) { - if (!doc) return NULL; - return doc->root; -} - -/** - * Get document encoding - */ -TAURUS_API const char* taurus_document_encoding(struct taurus_document* doc) { - if (!doc) return NULL; - return doc->encoding; /* May be NULL if not specified */ -} - -/* ============================================================================ - * Element Functions - * ============================================================================ */ - -/** - * Get element name - */ -TAURUS_API const char* taurus_element_name(struct taurus_element* elem) { - if (!elem) return ""; - return elem->name ? elem->name : ""; -} - -/** - * Get element text content (concatenated recursively) - */ -TAURUS_API const char* taurus_element_text(struct taurus_element* elem) { - if (!elem) return ""; - - /* Return direct text content if present */ - if (elem->text_content) { - return elem->text_content; - } - - /* If no direct text, check if we have children with text */ - /* For now, just return empty string - full recursive concatenation - * would require allocating memory, which complicates ownership */ - return ""; -} - -/* ============================================================================ - * XPath Functions - * ============================================================================ */ - -/** - * Evaluate XPath expression against document - */ -TAURUS_API struct taurus_xpath_result* taurus_xpath_eval( - struct taurus_document* doc, - const char* xpath_expr, - size_t expr_len -) { - if (!doc || !doc->root || !xpath_expr || expr_len == 0) { - return NULL; - } - - /* Parse XPath expression */ - XPathParser* parser = xpath_parser_new(xpath_expr, expr_len); - if (!parser) return NULL; - - XPathASTNode* ast = xpath_parse(parser); - const char* parse_error = xpath_parser_error(parser); - - if (!ast || parse_error) { - xpath_parser_free(parser); - return NULL; - } - - xpath_parser_free(parser); - - /* Create evaluation context */ - XPathContext* context = xpath_context_new(doc, doc->root); - if (!context) { - ast_node_free(ast); - return NULL; - } - - /* Evaluate expression */ - struct taurus_xpath_result* result = xpath_evaluate(context, ast); - - /* Check for evaluation errors */ - const char* eval_error = xpath_context_error(context); - if (eval_error && !result) { - /* Error already set in context */ - } - - /* Cleanup */ - xpath_context_free(context); - ast_node_free(ast); - - return result; -} - -/** - * Free XPath result - */ -TAURUS_API void taurus_xpath_result_free(struct taurus_xpath_result* result) { - /* Use internal xpath_result_free from evaluator */ - xpath_result_free(result); -} \ No newline at end of file diff --git a/ext/taurus/lib/src/taurus_internal.h b/ext/taurus/lib/src/taurus_internal.h deleted file mode 100644 index 2f61243..0000000 --- a/ext/taurus/lib/src/taurus_internal.h +++ /dev/null @@ -1,383 +0,0 @@ -/* libtaurus - Internal data structures - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * INTERNAL HEADER - Not part of public API - * These structures are implementation details and may change between versions. - */ - -#ifndef TAURUS_INTERNAL_H -#define TAURUS_INTERNAL_H - -#include -#include -#include -#include "taurus/types.h" /* For taurus_error_code */ - -/* ============================================================================ - * Internal Structures - Match ext/taurus/taurus.h but without Ruby - * ============================================================================ */ - -/* Processing instruction structure */ -struct taurus_processing_instruction { - char* target; /* PI target (e.g., "xml-stylesheet") */ - char* data; /* PI data/content */ - struct taurus_processing_instruction* next; /* Linked list */ -}; - -/* Document structure */ -struct taurus_document { - struct taurus_element* root; - char* encoding; /* UTF-8 assumed, but store if specified */ - struct taurus_processing_instruction* pis; /* Processing instructions */ - size_t ref_count; /* Reference counting for memory management */ -}; - -/* Element structure - Matches ext/taurus/taurus.h _element */ -struct taurus_element { - char* name; /* Element name (required) */ - char* prefix; /* Namespace prefix (can be NULL) */ - char* namespace_uri; /* Resolved namespace URI (can be NULL) */ - - /* Hierarchy */ - struct taurus_element* parent; - struct taurus_element** children; - size_t children_count; - size_t children_capacity; - - /* Attributes */ - struct taurus_attribute** attributes; - size_t attributes_count; - size_t attributes_capacity; - - /* Namespace declarations (linked list) */ - struct taurus_namespace* namespaces; - size_t namespaces_count; - size_t namespaces_capacity; - - /* Content */ - char* text_content; /* Concatenated text content */ - - /* Document order for XPath */ - long doc_order; /* -1 = unset, >= 0 = document order index */ -}; - -/* Attribute structure - Matches ext/taurus/taurus.h _attribute */ -struct taurus_attribute { - char* name; /* Attribute name (required) */ - char* prefix; /* Namespace prefix (can be NULL) */ - char* namespace_uri; /* Resolved namespace URI (can be NULL) */ - char* value; /* Attribute value (can be NULL for boolean attrs) */ -}; - -/* Namespace structure - Matches ext/taurus/taurus.h _namespace */ -struct taurus_namespace { - char* prefix; /* Namespace prefix (NULL = default namespace) */ - char* uri; /* Namespace URI (required) */ - struct taurus_namespace* next; /* Linked list for multiple declarations */ -}; - -/* ============================================================================ - * XPath Node Type System - * ============================================================================ */ - -/* Node type enumeration - supports all XPath node types */ -typedef enum { - TAURUS_NODE_ELEMENT = 0, - TAURUS_NODE_ATTRIBUTE = 1, - TAURUS_NODE_TEXT = 2, /* Future */ - TAURUS_NODE_COMMENT = 3, /* Future */ - TAURUS_NODE_PI = 4 /* Future */ -} TaurusNodeType; - -/* Attribute node structure - dedicated type for attribute nodes in XPath */ -typedef struct taurus_attribute_node { - TaurusNodeType node_type; /* Always TAURUS_NODE_ATTRIBUTE */ - char* name; /* Attribute name */ - char* value; /* Attribute value */ - char* namespace_uri; /* Namespace URI (can be NULL) */ - struct taurus_element* owner; /* Owner element */ -} TaurusAttributeNode; - -/* XPath node union - type-safe wrapper for all XPath node types */ -typedef union xpath_node { - TaurusNodeType type; /* First field for type checking */ - struct { - TaurusNodeType node_type; - struct taurus_element* element; - } as_element; - TaurusAttributeNode* as_attribute; -} XPathNode; - -/* Type checking macros */ -#define XPATH_NODE_TYPE(node) (*(TaurusNodeType*)(node)) -#define IS_ELEMENT_NODE(node) ((node) && XPATH_NODE_TYPE(node) == TAURUS_NODE_ELEMENT) -#define IS_ATTRIBUTE_NODE(node) ((node) && XPATH_NODE_TYPE(node) == TAURUS_NODE_ATTRIBUTE) - -/* ============================================================================ - * XPath Internal Structures - * ============================================================================ */ - -/* XPath token - Matches ext/taurus/xpath.h Token */ -typedef struct xpath_token { - int type; /* XPathTokenType */ - const char* value; /* Token value (points into input, not owned) */ - size_t value_len; - int line; - int column; -} XPathToken; - -/* XPath lexer - Matches ext/taurus/xpath.h Lexer */ -typedef struct xpath_lexer { - const char* input; /* Input string (not owned) */ - const char* pos; /* Current position */ - const char* end; /* End of input */ - int line; - int column; - XPathToken current; - char error_msg[256]; -} XPathLexer; - -/* XPath AST node types - From ext/taurus/xpath.h */ -typedef enum { - XPATH_AST_PATH_EXPR, - XPATH_AST_ABSOLUTE_PATH, - XPATH_AST_RELATIVE_PATH, - XPATH_AST_STEP, - XPATH_AST_AXIS_SPECIFIER, - XPATH_AST_NODE_TEST, - XPATH_AST_PREDICATE, - XPATH_AST_FUNCTION_CALL, - XPATH_AST_ARGUMENT, - XPATH_AST_NUMBER, - XPATH_AST_STRING, - XPATH_AST_VARIABLE_REFERENCE, - XPATH_AST_OPERATOR, - XPATH_AST_NODE_TEST_NAME, - XPATH_AST_NODE_TEST_TYPE, - XPATH_AST_NODE_TEST_PI, - XPATH_AST_NODE_TEST_ALL, - XPATH_AST_NODE_TEST_ALL_IN_NS -} XPathASTType; - -/* XPath AST node - Matches ext/taurus/xpath.h _xpath_ast_node */ -typedef struct xpath_ast_node { - XPathASTType type; - char* value; /* String value (owned by node) */ - double number_value; /* Number value */ - struct xpath_ast_node** children; - size_t child_count; - size_t child_capacity; - - /* Namespace support for node tests (v0.8.0) */ - char* prefix; /* Namespace prefix (NULL if no prefix) */ - char* local_name; /* Local name part (NULL if not applicable) */ -} XPathASTNode; - -/* XPath parser - Matches ext/taurus/xpath.h _xpath_parser */ -typedef struct xpath_parser { - XPathLexer* lexer; - XPathToken* tokens; /* Token array for lookahead */ - size_t token_count; - size_t token_pos; - char error_msg[256]; -} XPathParser; - -/* XPath nodeset - Holds typed node pointers (elements or attributes) */ -typedef struct xpath_nodeset { - void** nodes; /* Typed node pointers (element* or TaurusAttributeNode*) */ - size_t count; - size_t capacity; - int owns_attributes; /* If true, free attribute nodes on nodeset_free */ -} XPathNodeSet; - -/* XPath result types - From ext/taurus/xpath.h */ -typedef enum { - XPATH_RESULT_BOOLEAN, - XPATH_RESULT_NUMBER, - XPATH_RESULT_STRING, - XPATH_RESULT_NODESET -} XPathResultType; - -/* XPath result value union */ -typedef union { - int boolean_value; - double number_value; - char* string_value; /* Owned by result */ - XPathNodeSet* nodeset_value; /* Owned by result */ -} XPathResultValue; - -/* XPath result - Matches ext/taurus/xpath.h _xpath_result */ -struct taurus_xpath_result { - XPathResultType type; - XPathResultValue value; -}; - -/* Namespace mapping for XPath context (v0.8.0) */ -typedef struct xpath_namespace_mapping { - char* prefix; /* Namespace prefix (NULL = default namespace) */ - char* uri; /* Namespace URI (required) */ -} XPathNamespaceMapping; - -/* XPath context - Matches ext/taurus/xpath.h _xpath_context */ -typedef struct xpath_context { - struct taurus_document* document; - struct taurus_element* context_node; - size_t context_position; /* 1-based position in context nodeset */ - size_t context_size; /* Total size of context nodeset */ - void* function_registry; /* Opaque function registry */ - char error_msg[256]; - - /* Namespace support (v0.8.0) */ - XPathNamespaceMapping* namespace_mappings; - size_t namespace_count; - size_t namespace_capacity; - - /* Error context support (v1.0.0) */ - const char* input; /* Original XPath expression for error context */ - size_t input_len; /* Length of input expression */ - - /* Optimization flags */ - int to_boolean; /* Only checking existence */ - int max_results; /* Stop after N results (0 = unlimited) */ - int enable_early_exit; /* Master switch for early termination */ -} XPathContext; - -/* XPath operator types - From ext/taurus/xpath.h */ -typedef enum { - XPATH_OP_OR, - XPATH_OP_AND, - XPATH_OP_EQUAL, - XPATH_OP_NOT_EQUAL, - XPATH_OP_LESS, - XPATH_OP_LESS_EQUAL, - XPATH_OP_GREATER, - XPATH_OP_GREATER_EQUAL, - XPATH_OP_PLUS, - XPATH_OP_MINUS, - XPATH_OP_MULTIPLY, - XPATH_OP_DIV, - XPATH_OP_MOD, - XPATH_OP_UNION, - XPATH_OP_NEGATION -} XPathOperatorType; - -/* XPath axis types - From ext/taurus/xpath.h */ -typedef enum { - XPATH_AXIS_ANCESTOR, - XPATH_AXIS_ANCESTOR_OR_SELF, - XPATH_AXIS_ATTRIBUTE, - XPATH_AXIS_CHILD, - XPATH_AXIS_DESCENDANT, - XPATH_AXIS_DESCENDANT_OR_SELF, - XPATH_AXIS_FOLLOWING, - XPATH_AXIS_FOLLOWING_SIBLING, - XPATH_AXIS_NAMESPACE, - XPATH_AXIS_PARENT, - XPATH_AXIS_PRECEDING, - XPATH_AXIS_PRECEDING_SIBLING, - XPATH_AXIS_SELF -} XPathAxisType; - -/* ============================================================================ - * Memory Management Macros - * ============================================================================ */ - -/* Use standard C memory functions instead of Ruby macros */ -#define TAURUS_ALLOC(type) \ - ((type*)malloc(sizeof(type))) - -#define TAURUS_ALLOC_N(type, n) \ - ((type*)malloc(sizeof(type) * (n))) - -#define TAURUS_REALLOC_N(ptr, type, n) \ - ((type*)realloc((ptr), sizeof(type) * (n))) - -#define TAURUS_FREE(ptr) \ - do { if (ptr) { free(ptr); ptr = NULL; } } while(0) - -/* Array growth helper - double capacity when full */ -#define TAURUS_GROW_ARRAY(ptr, capacity) \ - do { \ - size_t new_cap = (capacity) == 0 ? 4 : (capacity) * 2; \ - (ptr) = realloc((ptr), new_cap * sizeof(*(ptr))); \ - (capacity) = new_cap; \ - } while(0) - -/* ============================================================================ - * Generic Memory Allocation - * ============================================================================ */ - -/* Generic malloc wrapper */ -static inline void* taurus_malloc(size_t size) { - return malloc(size); -} - -/* Generic realloc wrapper */ -static inline void* taurus_realloc(void* ptr, size_t size) { - return realloc(ptr, size); -} - -/* Generic free wrapper */ -static inline void taurus_free(void* ptr) { - free(ptr); -} - -/* ============================================================================ - * String Helpers - * ============================================================================ */ - -/* NULL-safe string duplication */ -static inline char* taurus_strdup(const char* str) { - if (!str) return NULL; - size_t len = strlen(str); - char* dup = (char*)malloc(len + 1); - if (dup) { - memcpy(dup, str, len + 1); - } - return dup; -} - -/* NULL-safe string length */ -static inline size_t taurus_strlen(const char* str) { - return str ? strlen(str) : 0; -} - -/* NULL-safe string comparison */ -static inline int taurus_strcmp(const char* s1, const char* s2) { - if (s1 == s2) return 0; - if (!s1) return -1; - if (!s2) return 1; - return strcmp(s1, s2); -} - -/* ============================================================================ - * Internal Error Functions (from error.c) - * ============================================================================ */ - -/* Set error with basic message */ -void taurus_set_error(taurus_error_code code, const char* message); - -/* Set error with line/column position */ -void taurus_set_parse_error_position(int line, int column); - -/* Set error with full context (message, input, position, snippet) */ -void taurus_set_error_with_context( - taurus_error_code code, - const char* message, - const char* input, - size_t byte_offset, - int line, - int column -); - -/* Extract context snippet from input around error position */ -void taurus_extract_context_snippet( - const char* input, - size_t offset, - int error_line, - char* out_buffer, - size_t buffer_size -); - -#endif /* TAURUS_INTERNAL_H */ \ No newline at end of file diff --git a/ext/taurus/lib/src/taurus_memory.c b/ext/taurus/lib/src/taurus_memory.c deleted file mode 100644 index 8a0d3be..0000000 --- a/ext/taurus/lib/src/taurus_memory.c +++ /dev/null @@ -1,423 +0,0 @@ -/* libtaurus - Memory management implementation - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - */ - -#include "taurus_memory.h" -#include -#include - -/* ============================================================================ - * Document Management - * ============================================================================ */ - -struct taurus_document* taurus_document_new(void) { - struct taurus_document* doc = TAURUS_ALLOC(struct taurus_document); - if (!doc) return NULL; - - doc->root = NULL; - doc->encoding = NULL; - doc->pis = NULL; - doc->ref_count = 1; - - return doc; -} - -void taurus_document_free_internal(struct taurus_document* doc) { - if (!doc) return; - - /* Free root element tree */ - if (doc->root) { - taurus_element_free_tree(doc->root); - } - - /* Free encoding string */ - if (doc->encoding) { - free(doc->encoding); - } - - /* Free processing instructions */ - if (doc->pis) { - taurus_pi_free_chain(doc->pis); - } - - free(doc); -} - -/* ============================================================================ - * Element Management - * ============================================================================ */ - -struct taurus_element* taurus_element_new(const char* name) { - if (!name) return NULL; - - struct taurus_element* elem = TAURUS_ALLOC(struct taurus_element); - if (!elem) return NULL; - - /* Initialize all fields */ - elem->name = taurus_strdup(name); - if (!elem->name) { - free(elem); - return NULL; - } - - elem->prefix = NULL; - elem->namespace_uri = NULL; - elem->parent = NULL; - elem->children = NULL; - elem->children_count = 0; - elem->children_capacity = 0; - elem->attributes = NULL; - elem->attributes_count = 0; - elem->attributes_capacity = 0; - elem->namespaces = NULL; - elem->namespaces_count = 0; - elem->namespaces_capacity = 0; - elem->text_content = NULL; - elem->doc_order = -1; - - return elem; -} - -void taurus_element_free_shallow(struct taurus_element* elem) { - size_t i; - - if (!elem) return; - - /* Free strings */ - if (elem->name) free(elem->name); - if (elem->prefix) free(elem->prefix); - if (elem->namespace_uri) free(elem->namespace_uri); - if (elem->text_content) free(elem->text_content); - - /* Free attributes */ - if (elem->attributes) { - for (i = 0; i < elem->attributes_count; i++) { - taurus_attribute_free(elem->attributes[i]); - } - free(elem->attributes); - } - - /* Free namespace chain */ - taurus_namespace_free_chain(elem->namespaces); - - /* Free children array (not elements themselves) */ - if (elem->children) { - free(elem->children); - } - - free(elem); -} - -void taurus_element_free_tree(struct taurus_element* elem) { - size_t i; - - if (!elem) return; - - /* Recursively free all children first */ - if (elem->children) { - for (i = 0; i < elem->children_count; i++) { - taurus_element_free_tree(elem->children[i]); - } - } - - /* Then free this element */ - taurus_element_free_shallow(elem); -} - -int taurus_element_add_child(struct taurus_element* parent, struct taurus_element* child) { - if (!parent || !child) return -1; - - /* Grow array if needed */ - if (parent->children_count >= parent->children_capacity) { - TAURUS_GROW_ARRAY(parent->children, parent->children_capacity); - if (!parent->children) return -1; - } - - /* Add child */ - parent->children[parent->children_count++] = child; - child->parent = parent; - - return 0; -} - -int taurus_element_add_attribute(struct taurus_element* elem, struct taurus_attribute* attr) { - if (!elem || !attr) return -1; - - /* Grow array if needed */ - if (elem->attributes_count >= elem->attributes_capacity) { - TAURUS_GROW_ARRAY(elem->attributes, elem->attributes_capacity); - if (!elem->attributes) return -1; - } - - /* Add attribute */ - elem->attributes[elem->attributes_count++] = attr; - - return 0; -} - -int taurus_element_add_namespace(struct taurus_element* elem, struct taurus_namespace* ns) { - if (!elem || !ns) return -1; - - /* Add to linked list at head */ - ns->next = elem->namespaces; - elem->namespaces = ns; - elem->namespaces_count++; - - return 0; -} - -/* ============================================================================ - * Attribute Management - * ============================================================================ */ - -struct taurus_attribute* taurus_attribute_new(const char* name, const char* value) { - if (!name) return NULL; - - struct taurus_attribute* attr = TAURUS_ALLOC(struct taurus_attribute); - if (!attr) return NULL; - - attr->name = taurus_strdup(name); - if (!attr->name) { - free(attr); - return NULL; - } - - attr->prefix = NULL; - attr->namespace_uri = NULL; - attr->value = value ? taurus_strdup(value) : NULL; - - if (value && !attr->value) { - free(attr->name); - free(attr); - return NULL; - } - - return attr; -} - -void taurus_attribute_free(struct taurus_attribute* attr) { - if (!attr) return; - - if (attr->name) free(attr->name); - if (attr->prefix) free(attr->prefix); - if (attr->namespace_uri) free(attr->namespace_uri); - if (attr->value) free(attr->value); - - free(attr); -} - -/* ============================================================================ - * Namespace Management - * ============================================================================ */ - -struct taurus_namespace* taurus_namespace_new(const char* prefix, const char* uri) { - if (!uri) return NULL; - - struct taurus_namespace* ns = TAURUS_ALLOC(struct taurus_namespace); - if (!ns) return NULL; - - ns->prefix = prefix ? taurus_strdup(prefix) : NULL; - ns->uri = taurus_strdup(uri); - ns->next = NULL; - - if (!ns->uri || (prefix && !ns->prefix)) { - if (ns->prefix) free(ns->prefix); - if (ns->uri) free(ns->uri); - free(ns); - return NULL; - } - - return ns; -} - -void taurus_namespace_free_single(struct taurus_namespace* ns) { - if (!ns) return; - - if (ns->prefix) free(ns->prefix); - if (ns->uri) free(ns->uri); - free(ns); -} - -void taurus_namespace_free_chain(struct taurus_namespace* ns) { - struct taurus_namespace* next; - - while (ns) { - next = ns->next; - taurus_namespace_free_single(ns); - ns = next; - } -} - -struct taurus_namespace* taurus_namespace_find(struct taurus_element* elem, const char* prefix) { - struct taurus_namespace* ns; - - if (!elem) return NULL; - - /* Search in current element's namespace declarations */ - for (ns = elem->namespaces; ns; ns = ns->next) { - if (!prefix && !ns->prefix) { - /* Both NULL (default namespace) */ - return ns; - } - if (prefix && ns->prefix && strcmp(prefix, ns->prefix) == 0) { - /* Matching prefixes */ - return ns; - } - } - - /* Not found in current element, check parent (inheritance) */ - if (elem->parent) { - return taurus_namespace_find(elem->parent, prefix); - } - - return NULL; -} - -/* ============================================================================ - * XPath Memory Management - * ============================================================================ */ - -XPathNodeSet* taurus_xpath_nodeset_new(void) { - return taurus_xpath_nodeset_new_with_capacity(4); -} - -XPathNodeSet* taurus_xpath_nodeset_new_with_capacity(size_t capacity) { - XPathNodeSet* nodeset = TAURUS_ALLOC(XPathNodeSet); - if (!nodeset) return NULL; - - nodeset->nodes = TAURUS_ALLOC_N(struct taurus_element*, capacity); - if (!nodeset->nodes) { - free(nodeset); - return NULL; - } - - nodeset->count = 0; - nodeset->capacity = capacity; - - return nodeset; -} - -int taurus_xpath_nodeset_add(XPathNodeSet* nodeset, struct taurus_element* node) { - if (!nodeset || !node) return -1; - - /* Grow array if needed */ - if (nodeset->count >= nodeset->capacity) { - size_t new_cap = nodeset->capacity * 2; - struct taurus_element** new_nodes = TAURUS_REALLOC_N(nodeset->nodes, struct taurus_element*, new_cap); - if (!new_nodes) return -1; - - nodeset->nodes = new_nodes; - nodeset->capacity = new_cap; - } - - /* Add node */ - nodeset->nodes[nodeset->count++] = node; - - return 0; -} - -void taurus_xpath_nodeset_free(XPathNodeSet* nodeset) { - if (!nodeset) return; - - if (nodeset->nodes) { - free(nodeset->nodes); - } - - free(nodeset); -} - -struct taurus_xpath_result* taurus_xpath_result_new(XPathResultType type) { - struct taurus_xpath_result* result = TAURUS_ALLOC(struct taurus_xpath_result); - if (!result) return NULL; - - result->type = type; - - /* Initialize value union based on type */ - switch (type) { - case XPATH_RESULT_BOOLEAN: - result->value.boolean_value = 0; - break; - case XPATH_RESULT_NUMBER: - result->value.number_value = 0.0; - break; - case XPATH_RESULT_STRING: - result->value.string_value = NULL; - break; - case XPATH_RESULT_NODESET: - result->value.nodeset_value = NULL; - break; - } - - return result; -} - -void taurus_xpath_result_free_internal(struct taurus_xpath_result* result) { - if (!result) return; - - /* Free owned data based on type */ - switch (result->type) { - case XPATH_RESULT_STRING: - if (result->value.string_value) { - free(result->value.string_value); - } - break; - case XPATH_RESULT_NODESET: - if (result->value.nodeset_value) { - taurus_xpath_nodeset_free(result->value.nodeset_value); - } - break; - default: - /* Boolean and number don't own data */ - break; - } - - free(result); -} - -/* ============================================================================ - * Processing Instruction Management - * ============================================================================ */ - -struct taurus_processing_instruction* taurus_pi_new(const char* target, const char* data) { - if (!target) return NULL; - - struct taurus_processing_instruction* pi = TAURUS_ALLOC(struct taurus_processing_instruction); - if (!pi) return NULL; - - pi->target = taurus_strdup(target); - if (!pi->target) { - free(pi); - return NULL; - } - - pi->data = data ? taurus_strdup(data) : NULL; - if (data && !pi->data) { - free(pi->target); - free(pi); - return NULL; - } - - pi->next = NULL; - - return pi; -} - -void taurus_pi_free(struct taurus_processing_instruction* pi) { - if (!pi) return; - - if (pi->target) free(pi->target); - if (pi->data) free(pi->data); - - free(pi); -} - -void taurus_pi_free_chain(struct taurus_processing_instruction* pi) { - struct taurus_processing_instruction* next; - - while (pi) { - next = pi->next; - taurus_pi_free(pi); - pi = next; - } -} \ No newline at end of file diff --git a/ext/taurus/lib/src/taurus_memory.h b/ext/taurus/lib/src/taurus_memory.h deleted file mode 100644 index 5e188b8..0000000 --- a/ext/taurus/lib/src/taurus_memory.h +++ /dev/null @@ -1,194 +0,0 @@ -/* libtaurus - Memory management - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * Internal memory management functions - */ - -#ifndef TAURUS_MEMORY_H -#define TAURUS_MEMORY_H - -#include "taurus_internal.h" - -/* ============================================================================ - * Document Management - * ============================================================================ */ - -/** - * Create new document - * @return Document or NULL on allocation failure - */ -struct taurus_document* taurus_document_new(void); - -/** - * Free document and all elements (internal implementation) - * @param doc Document to free - */ -void taurus_document_free_internal(struct taurus_document* doc); - -/* ============================================================================ - * Element Management - * ============================================================================ */ - -/** - * Create new element - * @param name Element name (will be copied) - * @return Element or NULL on allocation failure - */ -struct taurus_element* taurus_element_new(const char* name); - -/** - * Free element (non-recursive, doesn't free children) - * @param elem Element to free - */ -void taurus_element_free_shallow(struct taurus_element* elem); - -/** - * Free element and entire subtree recursively - * @param elem Element to free - */ -void taurus_element_free_tree(struct taurus_element* elem); - -/** - * Add child element to parent - * @param parent Parent element - * @param child Child element - * @return 0 on success, -1 on allocation failure - */ -int taurus_element_add_child(struct taurus_element* parent, struct taurus_element* child); - -/** - * Add attribute to element - * @param elem Element - * @param attr Attribute (ownership transferred to element) - * @return 0 on success, -1 on allocation failure - */ -int taurus_element_add_attribute(struct taurus_element* elem, struct taurus_attribute* attr); - -/** - * Add namespace declaration to element - * @param elem Element - * @param ns Namespace (ownership transferred to element) - * @return 0 on success, -1 on allocation failure - */ -int taurus_element_add_namespace(struct taurus_element* elem, struct taurus_namespace* ns); - -/* ============================================================================ - * Attribute Management - * ============================================================================ */ - -/** - * Create new attribute - * @param name Attribute name (will be copied) - * @param value Attribute value (will be copied, can be NULL) - * @return Attribute or NULL on allocation failure - */ -struct taurus_attribute* taurus_attribute_new(const char* name, const char* value); - -/** - * Free attribute - * @param attr Attribute to free - */ -void taurus_attribute_free(struct taurus_attribute* attr); - -/* ============================================================================ - * Namespace Management - * ============================================================================ */ - -/** - * Create new namespace - * @param prefix Namespace prefix (will be copied, NULL for default namespace) - * @param uri Namespace URI (will be copied, required) - * @return Namespace or NULL on allocation failure - */ -struct taurus_namespace* taurus_namespace_new(const char* prefix, const char* uri); - -/** - * Free namespace (non-recursive, doesn't free next) - * @param ns Namespace to free - */ -void taurus_namespace_free_single(struct taurus_namespace* ns); - -/** - * Free namespace chain (recursive, frees entire linked list) - * @param ns First namespace in chain - */ -void taurus_namespace_free_chain(struct taurus_namespace* ns); - -/** - * Find namespace by prefix in element (with inheritance) - * @param elem Element to start search from - * @param prefix Prefix to find (NULL for default namespace) - * @return Namespace or NULL if not found - */ -struct taurus_namespace* taurus_namespace_find(struct taurus_element* elem, const char* prefix); - -/* ============================================================================ - * XPath Memory Management - * ============================================================================ */ - -/** - * Create new XPath nodeset - * @return Nodeset or NULL on allocation failure - */ -XPathNodeSet* taurus_xpath_nodeset_new(void); - -/** - * Create new XPath nodeset with initial capacity - * @param capacity Initial capacity - * @return Nodeset or NULL on allocation failure - */ -XPathNodeSet* taurus_xpath_nodeset_new_with_capacity(size_t capacity); - -/** - * Add node to nodeset - * @param nodeset Nodeset - * @param node Element to add - * @return 0 on success, -1 on allocation failure - */ -int taurus_xpath_nodeset_add(XPathNodeSet* nodeset, struct taurus_element* node); - -/** - * Free nodeset (doesn't free the elements themselves) - * @param nodeset Nodeset to free - */ -void taurus_xpath_nodeset_free(XPathNodeSet* nodeset); - -/** - * Create new XPath result - * @param type Result type - * @return Result or NULL on allocation failure - */ -struct taurus_xpath_result* taurus_xpath_result_new(XPathResultType type); - -/** - * Free XPath result (frees owned data) - * @param result Result to free - */ -void taurus_xpath_result_free_internal(struct taurus_xpath_result* result); - -/* ============================================================================ - * Processing Instruction Management - * ============================================================================ */ - -/** - * Create new processing instruction - * @param target PI target (will be copied) - * @param data PI data (will be copied) - * @return Processing instruction or NULL on allocation failure - */ -struct taurus_processing_instruction* taurus_pi_new(const char* target, const char* data); - -/** - * Free processing instruction (single, doesn't free next) - * @param pi Processing instruction to free - */ -void taurus_pi_free(struct taurus_processing_instruction* pi); - -/** - * Free processing instruction chain (recursive, frees entire linked list) - * @param pi First processing instruction in chain - */ -void taurus_pi_free_chain(struct taurus_processing_instruction* pi); - -#endif /* TAURUS_MEMORY_H */ \ No newline at end of file diff --git a/ext/taurus/lib/src/taurus_parse.c b/ext/taurus/lib/src/taurus_parse.c deleted file mode 100644 index b532117..0000000 --- a/ext/taurus/lib/src/taurus_parse.c +++ /dev/null @@ -1,382 +0,0 @@ -/* taurus_parse.c - XML parser core helper functions - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * Converted from ext/taurus/parse.c (Ruby C extension → pure C library) - * Session 82: Helper functions (parse_name, parse_quoted_value, etc.) - * Session 84: Modularized - moved element/content/document functions to separate files - */ - -#include "parse_internal.h" -#include -#include -#include - -/* ================================================================== - * STRING HELPERS - * ================================================================== */ - -/* Duplicate string from buffer with length (NOT null-terminated in source) */ -char *taurus_strndup(const char *str, size_t len) { - char *dup; - - if (!str) return NULL; - - dup = (char*)taurus_malloc(len + 1); - if (!dup) return NULL; - - memcpy(dup, str, len); - dup[len] = '\0'; - - return dup; -} - -/* Extract prefix and local name from qualified name "prefix:local" - * Returns 0 if no prefix, 1 if prefix found - * Modifies qname buffer in place, caller must free qname */ -int extract_prefix_and_local(char *qname, char **prefix, char **local_name) { - char *colon; - - if (!qname || !prefix || !local_name) return -1; - - /* Find colon separator */ - colon = strchr(qname, ':'); - - if (!colon) { - /* No prefix */ - *prefix = NULL; - *local_name = qname; - return 0; - } - - /* Split at colon */ - *colon = '\0'; - *prefix = qname; - *local_name = colon + 1; - - return 1; -} - -/* Extract just local name from qualified name (for end tag comparison) */ -const char *extract_local_name_only(const char *qname) { - const char *colon; - - if (!qname) return NULL; - - colon = strchr(qname, ':'); - if (!colon) { - return qname; /* No prefix, return whole name */ - } - - return colon + 1; /* Return part after colon */ -} - - -/* ================================================================== - * PARSE CONTEXT MANAGEMENT - * ================================================================== */ - -/* Initialize parse context with input buffer and options */ -int taurus_parse_context_init(TaurusParseContext *ctx, - const char *xml, - size_t len, - TaurusParseOptions *opts) { - if (!ctx || !xml) { - return -1; - } - - /* Initialize input buffer pointers */ - ctx->start = xml; - ctx->pos = xml; - ctx->end = xml + len; - - /* Initialize parse state */ - ctx->doc = NULL; - ctx->current = NULL; - taurus_attr_stack_init(&ctx->attr_stack); - string_intern_table_init(&ctx->intern_table); - - /* Set options (use defaults if NULL) */ - if (opts) { - ctx->opts = *opts; - } else { - taurus_parse_options_init(&ctx->opts); - } - - /* Initialize error tracking */ - ctx->line = 1; - ctx->column = 1; - ctx->error[0] = '\0'; - - return 0; -} - -/* Free parse context resources */ -void taurus_parse_context_free(TaurusParseContext *ctx) { - if (!ctx) { - return; - } - - /* Cleanup attribute stack (frees heap memory if allocated) */ - taurus_attr_stack_cleanup(&ctx->attr_stack); - - /* Free string interning table */ - string_intern_table_free(&ctx->intern_table); - - /* Clear pointers (safety) */ - ctx->start = NULL; - ctx->pos = NULL; - ctx->end = NULL; - ctx->doc = NULL; - ctx->current = NULL; -} - -/* ================================================================== - * CORE PARSING FUNCTIONS - * ================================================================== */ - -/* Parse an XML name (element or attribute name) - * - * Returns pointer to name in buffer and sets *len to name length. - * Advances ctx->pos past the name. - * Returns NULL on error (sets ctx->error). - * - * XML name rules: - * - Must start with letter, underscore, or colon - * - Can contain letters, digits, hyphens, periods, colons, underscores - * - * NOTE: Returned pointer is into parse buffer, NOT null-terminated. - * Caller must copy with taurus_strndup() if needed beyond parse. - */ -const char *parse_name(TaurusParseContext *ctx, size_t *len) { - const char *start; - - /* Skip leading whitespace */ - taurus_skip_whitespace(&ctx->pos, ctx->end); - - /* Set start pointer */ - start = ctx->pos; - - /* Must start with letter or underscore (colon for namespaces) */ - if (ctx->pos >= ctx->end || - (!is_name_start_fast(*ctx->pos) && *ctx->pos != ':')) { - taurus_parse_context_set_error(ctx, - "Invalid name start character at line %d, column %d", - ctx->line, ctx->column); - return NULL; - } - ctx->pos++; - - /* Parse name characters using fast table lookup */ - while (ctx->pos < ctx->end && taurus_is_name_char(*ctx->pos)) { - ctx->pos++; - } - - /* Calculate length */ - *len = ctx->pos - start; - - /* Name cannot be empty */ - if (*len == 0) { - taurus_parse_context_set_error(ctx, - "Empty name at line %d, column %d", - ctx->line, ctx->column); - return NULL; - } - - return start; -} - -/* Parse a quoted attribute value - * - * Returns pointer to value in buffer (excluding quotes) and sets *len. - * Advances ctx->pos past the closing quote. - * Returns NULL on error (sets ctx->error). - * - * Handles both single and double quotes. - * Uses SIMD optimization to find closing quote quickly. - * - * NOTE: Returned pointer is into parse buffer, NOT null-terminated. - * Caller must copy with taurus_strndup() if needed. - */ -const char *parse_quoted_value(TaurusParseContext *ctx, size_t *len) { - char quote; - const char *value_start; - const char *quote_pos; - - /* Skip leading whitespace */ - taurus_skip_whitespace(&ctx->pos, ctx->end); - - /* Must start with quote */ - if (ctx->pos >= ctx->end || - (*ctx->pos != '"' && *ctx->pos != '\'')) { - taurus_parse_context_set_error(ctx, - "Expected quote at line %d, column %d", - ctx->line, ctx->column); - return NULL; - } - - /* Save quote character and skip it */ - quote = *ctx->pos++; - value_start = ctx->pos; - - /* Find closing quote using SIMD optimization */ - quote_pos = simd_find_char(ctx->pos, ctx->end, quote); - if (!quote_pos) { - taurus_parse_context_set_error(ctx, - "Unterminated quoted value at line %d, column %d", - ctx->line, ctx->column); - return NULL; - } - - /* Calculate value length (excluding quotes) */ - *len = quote_pos - value_start; - - /* Advance position past closing quote */ - ctx->pos = quote_pos + 1; - - return value_start; -} - -/* Skip XML comment - * - * Returns 0 on success, -1 on error (sets ctx->error). - * Advances ctx->pos past the comment end marker "-->". - * - * Assumes ctx->pos is at '" pattern */ - while (ctx->pos + 2 < ctx->end) { - /* Use SIMD to find next '-' quickly */ - dash_pos = simd_find_char(ctx->pos, ctx->end - 2, '-'); - if (!dash_pos) { - /* No more dashes - unterminated comment */ - break; - } - - /* Move to dash position */ - ctx->pos = dash_pos; - - /* Check if it's "-->" */ - if (ctx->pos[0] == '-' && ctx->pos[1] == '-' && ctx->pos[2] == '>') { - ctx->pos += 3; /* Skip "-->" */ - return 0; - } - - /* Not a match, move past this dash and continue */ - ctx->pos++; - } - - /* Unterminated comment */ - taurus_parse_context_set_error(ctx, - "Unterminated comment at line %d, column %d", - ctx->line, ctx->column); - return -1; -} - -/* Parse CDATA section - * - * Returns pointer to CDATA content (excluding markers) and sets *len. - * Advances ctx->pos past the "]]>" end marker. - * Returns NULL on error (sets ctx->error). - * - * Assumes ctx->pos is at "pos; - - /* Scan for "]]>" pattern */ - while (ctx->pos + 2 < ctx->end) { - /* Use SIMD to find next ']' quickly */ - bracket_pos = simd_find_char(ctx->pos, ctx->end - 2, ']'); - if (!bracket_pos) { - /* No more brackets - unterminated CDATA */ - break; - } - - /* Move to bracket position */ - ctx->pos = bracket_pos; - - /* Check if it's "]]>" */ - if (ctx->pos[0] == ']' && ctx->pos[1] == ']' && ctx->pos[2] == '>') { - /* Found end marker */ - *len = ctx->pos - content_start; - ctx->pos += 3; /* Skip "]]>" */ - return content_start; - } - - /* Not a match, move past this bracket and continue */ - ctx->pos++; - } - - /* Unterminated CDATA */ - taurus_parse_context_set_error(ctx, - "Unterminated CDATA section at line %d, column %d", - ctx->line, ctx->column); - return NULL; -} - -/* Parse text content (until '<' or end of buffer) - * - * Returns pointer to text content and sets *len. - * Advances ctx->pos to the '<' character or end. - * Returns NULL if text is empty or only whitespace (unless preserve_whitespace). - * - * Uses SIMD optimization to find '<' character quickly. - * - * NOTE: Returned pointer is into parse buffer, NOT null-terminated. - */ -const char *parse_text(TaurusParseContext *ctx, size_t *len) { - const char *start; - const char *text_end; - const char *p; - int has_non_whitespace; - - start = ctx->pos; - has_non_whitespace = 0; - - /* Find next '<' using SIMD */ - text_end = simd_find_char(ctx->pos, ctx->end, '<'); - if (!text_end) { - /* No '<' found - text goes to end of input */ - text_end = ctx->end; - } - - /* Check for non-whitespace content */ - p = ctx->pos; - while (p < text_end && !has_non_whitespace) { - if (!taurus_is_whitespace(*p)) { - has_non_whitespace = 1; - } - p++; - } - - /* Advance position */ - ctx->pos = text_end; - - /* Calculate length */ - *len = text_end - start; - - /* Return NULL if only whitespace (unless preserving) */ - if (!has_non_whitespace && !ctx->opts.preserve_whitespace) { - return NULL; - } - - /* Return NULL if empty */ - if (*len == 0) { - return NULL; - } - - return start; -} \ No newline at end of file diff --git a/ext/taurus/lib/src/taurus_parse.h b/ext/taurus/lib/src/taurus_parse.h deleted file mode 100644 index f511fda..0000000 --- a/ext/taurus/lib/src/taurus_parse.h +++ /dev/null @@ -1,192 +0,0 @@ -/* taurus_parse.h - XML parser for libtaurus (pure C) - * Copyright (c) 2024, Ribose Inc. - * All rights reserved. - * - * Converted from ext/taurus/parse.c (Ruby C extension → pure C library) - */ - -#ifndef TAURUS_PARSE_H -#define TAURUS_PARSE_H - -#include "taurus_internal.h" -#include "parse_helpers.h" -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* ================================================================== - * PARSE OPTIONS - * ================================================================== */ - -/* Parse options structure */ -typedef struct taurus_parse_options { - int strict; /* Strict XML validation (default: 1) */ - int preserve_whitespace; /* Preserve whitespace-only text nodes (default: 0) */ - int track_positions; /* Track line/column positions (default: 0) */ -} TaurusParseOptions; - -/* Initialize parse options with defaults */ -static inline void taurus_parse_options_init(TaurusParseOptions *opts) { - opts->strict = 1; - opts->preserve_whitespace = 0; - opts->track_positions = 0; -} - -/* ================================================================== - * PARSE CONTEXT - * ================================================================== */ - -/* Parse context - internal state during parsing - * NOTE: This structure is opaque to users. Use accessor functions. */ -typedef struct taurus_parse_context { - /* Input buffer */ - const char *start; /* Start of input (for error reporting) */ - const char *pos; /* Current position */ - const char *end; /* End of input */ - - /* Parse state */ - struct taurus_document *doc; /* Document being built */ - struct taurus_element *current; /* Current element (for nesting) */ - TaurusAttrStack attr_stack; /* Reusable attribute stack */ - StringInternTable intern_table; /* String interning for attributes */ - - /* Options */ - TaurusParseOptions opts; - - /* Error tracking */ - int line; /* Current line number (1-based) */ - int column; /* Current column number (1-based) */ - char error[256]; /* Error message (empty if no error) */ -} TaurusParseContext; - -/* Initialize parse context with input buffer and options - * Returns 0 on success, -1 on error */ -int taurus_parse_context_init(TaurusParseContext *ctx, - const char *xml, - size_t len, - TaurusParseOptions *opts); - -/* Free parse context resources (attributes, interning table, etc.) - * Does NOT free the document (caller owns that) */ -void taurus_parse_context_free(TaurusParseContext *ctx); - -/* Get error message from parse context (empty string if no error) */ -static inline const char *taurus_parse_context_error(const TaurusParseContext *ctx) { - return ctx->error; -} - -/* Set error message in parse context */ -static inline void taurus_parse_context_set_error(TaurusParseContext *ctx, - const char *fmt, ...) { - va_list args; - va_start(args, fmt); - vsnprintf(ctx->error, sizeof(ctx->error), fmt, args); - va_end(args); -} - -/* ================================================================== - * MAIN PARSE API - * ================================================================== */ - -/* Parse XML string into document structure - * - * Parameters: - * xml: XML string to parse (need not be null-terminated) - * len: Length of XML string - * opts: Parse options (NULL for defaults) - * - * Returns: - * Document structure on success, NULL on error - * Caller owns returned document and must free with taurus_document_free_tree() - * - * Thread safety: Each parse operation is independent (no shared state) - * - * Example: - * const char *xml = "text"; - * struct taurus_document *doc = taurus_parse(xml, strlen(xml), NULL); - * if (!doc) { - * fprintf(stderr, "Parse error\n"); - * return; - * } - * // Use document... - * taurus_document_free_tree(doc); - */ -struct taurus_document *taurus_parse(const char *xml, - size_t len, - TaurusParseOptions *opts); - -/* Parse XML with error reporting - * - * Parameters: - * xml: XML string to parse - * len: Length of XML string - * opts: Parse options (NULL for defaults) - * error_buf: Buffer for error message (can be NULL) - * error_len: Size of error buffer - * - * Returns: - * Document structure on success, NULL on error - * If error_buf is provided, it will contain error message on failure - * - * Example: - * char error[256]; - * struct taurus_document *doc = taurus_parse_with_error( - * xml, len, NULL, error, sizeof(error) - * ); - * if (!doc) { - * fprintf(stderr, "Parse error: %s\n", error); - * return; - * } - */ -struct taurus_document *taurus_parse_with_error(const char *xml, - size_t len, - TaurusParseOptions *opts, - char *error_buf, - size_t error_len); - -/* ================================================================== - * HELPER FUNCTIONS (exposed for testing) - * ================================================================== */ - -/* Parse XML name - exposed for testing */ -const char *parse_name(TaurusParseContext *ctx, size_t *len); - -/* Parse quoted attribute value - exposed for testing */ -const char *parse_quoted_value(TaurusParseContext *ctx, size_t *len); - -/* Skip XML comment - exposed for testing */ -int skip_comment(TaurusParseContext *ctx); - -/* Parse CDATA section - exposed for testing */ -const char *parse_cdata(TaurusParseContext *ctx, size_t *len); - -/* Parse text content - exposed for testing */ -const char *parse_text(TaurusParseContext *ctx, size_t *len); - -/* ================================================================== - * ELEMENT PARSING FUNCTIONS (exposed for testing - Session 83) - * ================================================================== */ - -/* Parse element start tag - * Returns newly created element or NULL on error - * NOTE: Return value may have lowest bit set to indicate self-closing */ -struct taurus_element *parse_start_tag(TaurusParseContext *ctx, - struct taurus_element *parent); - -/* Parse element end tag - * Returns 0 on success, -1 on error */ -int parse_end_tag(TaurusParseContext *ctx, const char *expected_name); - -/* Parse complete element (start tag, content, end tag) - * Returns newly created element or NULL on error */ -struct taurus_element *parse_element(TaurusParseContext *ctx, - struct taurus_element *parent); - -#ifdef __cplusplus -} -#endif - -#endif /* TAURUS_PARSE_H */ \ No newline at end of file diff --git a/ext/taurus/lib/src/xpath/evaluator.c b/ext/taurus/lib/src/xpath/evaluator.c deleted file mode 100644 index cd87930..0000000 --- a/ext/taurus/lib/src/xpath/evaluator.c +++ /dev/null @@ -1,1931 +0,0 @@ -/* evaluator.c - XPath evaluator implementation - * Copyright (c) 2024, Ribose Inc. - * - * Pure C implementation of XPath 1.0 evaluator. - * Converted from Ruby C extension to pure C. - */ - -#include "evaluator.h" -#include "functions.h" -#include "lexer.h" -#include "parser.h" -#include "taurus/taurus.h" -#include -#include -#include -#include - -/* Debug logging - set to 1 to enable */ -#define XPATH_DEBUG 0 - -#if XPATH_DEBUG -#define DEBUG_LOG(fmt, ...) fprintf(stderr, "[XPath DEBUG] " fmt "\n", ##__VA_ARGS__) -#else -#define DEBUG_LOG(fmt, ...) do {} while(0) -#endif - -/* ============================================================================ - * Forward Declarations - * ============================================================================ */ - -/* Namespace support functions (v0.8.0) */ -static void xpath_context_register_namespace(XPathContext* context, - const char* prefix, - const char* uri); -static const char* xpath_context_resolve_prefix(XPathContext* context, - const char* prefix); -static void xpath_context_init_from_document(XPathContext* context); - -/* Main evaluation dispatcher */ -static struct taurus_xpath_result* evaluate_expr(XPathContext* context, - XPathASTNode* ast); - -/* Path evaluation */ -static struct taurus_xpath_result* evaluate_location_path(XPathContext* context, - XPathASTNode* path); -static struct taurus_xpath_result* evaluate_step(XPathContext* context, - XPathASTNode* step, - XPathNodeSet* input); - -/* Node test matching */ -static int matches_node_test(XPathContext* ctx, struct taurus_element* node, XPathASTNode* test); - -/* Predicate evaluation */ -static XPathNodeSet* apply_predicates(XPathContext* context, - XPathNodeSet* nodes, - XPathASTNode** predicates, - size_t pred_count); - -/* Axis implementations */ -static XPathNodeSet* apply_axis(XPathContext* context, - struct taurus_element* node, - const char* axis_name, - XPathASTNode* node_test); - -/* Operators */ -static struct taurus_xpath_result* evaluate_operator(XPathContext* context, - XPathASTNode* ast); - -/* ============================================================================ - * Context Management - * ============================================================================ */ - -XPathContext* xpath_context_new(struct taurus_document* document, - struct taurus_element* context_node) { - if (!document || !context_node) return NULL; - - XPathContext* context = TAURUS_ALLOC(XPathContext); - if (!context) return NULL; - - context->document = document; - context->context_node = context_node; - context->context_position = 1; - context->context_size = 1; - context->error_msg[0] = '\0'; - context->to_boolean = 0; - context->max_results = 0; - context->enable_early_exit = 1; - - /* Initialize namespace support (v0.8.0) */ - context->namespace_mappings = NULL; - context->namespace_count = 0; - context->namespace_capacity = 0; - - /* Initialize error context (v1.0.0) */ - context->input = NULL; - context->input_len = 0; - - /* Initialize function registry with standard XPath 1.0 functions */ - context->function_registry = xpath_function_registry_new(); - if (context->function_registry) { - xpath_function_registry_init_standard( - (XPathFunctionRegistry*)context->function_registry); - } - - /* Auto-populate namespace mappings from document (v0.8.0) */ - xpath_context_init_from_document(context); - - return context; -} - -void xpath_context_free(XPathContext* context) { - if (!context) return; - - /* Free namespace mappings (v0.8.0) */ - if (context->namespace_mappings) { - for (size_t i = 0; i < context->namespace_count; i++) { - TAURUS_FREE(context->namespace_mappings[i].prefix); - TAURUS_FREE(context->namespace_mappings[i].uri); - } - TAURUS_FREE(context->namespace_mappings); - } - - /* Free function registry */ - if (context->function_registry) { - xpath_function_registry_free((XPathFunctionRegistry*)context->function_registry); - } - - TAURUS_FREE(context); -} - -const char* xpath_context_error(XPathContext* context) { - if (!context) return "Invalid context"; - return context->error_msg[0] ? context->error_msg : NULL; -} - -/* ============================================================================ - * Namespace Support (v0.8.0) - * ============================================================================ */ - -/** - * Register a namespace prefix->URI mapping in the context - * - * @param context XPath context - * @param prefix Namespace prefix (NULL for default namespace) - * @param uri Namespace URI (required) - */ -void xpath_context_register_namespace(XPathContext* context, - const char* prefix, - const char* uri) { - if (!context || !uri) return; - - /* Check if prefix already registered - update if found */ - for (size_t i = 0; i < context->namespace_count; i++) { - int prefix_matches = 0; - if (!prefix && !context->namespace_mappings[i].prefix) { - prefix_matches = 1; /* Both NULL (default namespace) */ - } else if (prefix && context->namespace_mappings[i].prefix && - strcmp(prefix, context->namespace_mappings[i].prefix) == 0) { - prefix_matches = 1; /* Both non-NULL and equal */ - } - - if (prefix_matches) { - /* Update existing mapping */ - TAURUS_FREE(context->namespace_mappings[i].uri); - context->namespace_mappings[i].uri = taurus_strdup(uri); - return; - } - } - - /* Add new mapping - grow array if needed */ - if (context->namespace_count >= context->namespace_capacity) { - size_t new_capacity = context->namespace_capacity == 0 ? - 4 : context->namespace_capacity * 2; - XPathNamespaceMapping* new_mappings = TAURUS_REALLOC_N( - context->namespace_mappings, - XPathNamespaceMapping, - new_capacity - ); - if (!new_mappings) return; /* Allocation failed */ - - context->namespace_mappings = new_mappings; - context->namespace_capacity = new_capacity; - } - - /* Add new mapping */ - context->namespace_mappings[context->namespace_count].prefix = - prefix ? taurus_strdup(prefix) : NULL; - context->namespace_mappings[context->namespace_count].uri = - taurus_strdup(uri); - context->namespace_count++; -} - -/** - * Resolve namespace prefix to URI (OPTIMIZED with reverse lookup) - * - * Strategy: Search from END to START to find most recent registration first. - * This handles override semantics naturally - child namespace declarations - * override parent ones because they're registered later. - * - * Performance: O(n) worst case, but in practice very fast because: - * - Most documents have <10 unique namespace prefixes - * - Recent registrations (local scope) found first - * - Common prefixes cached by compiler in registers - * - * @param context XPath context - * @param prefix Namespace prefix to resolve (NULL for default namespace) - * @return Namespace URI, or NULL if not found - */ -const char* xpath_context_resolve_prefix(XPathContext* context, - const char* prefix) { - if (!context || context->namespace_count == 0) return NULL; - - /* Search BACKWARDS for most recent (local) registration first - * This implements namespace scope override semantics efficiently */ - for (size_t i = context->namespace_count; i > 0; i--) { - size_t idx = i - 1; - XPathNamespaceMapping* mapping = &context->namespace_mappings[idx]; - - /* Fast path: Compare prefix pointers first (common case: same string object) */ - if (mapping->prefix == prefix) { - return mapping->uri; - } - - /* Both NULL = default namespace match */ - if (!prefix && !mapping->prefix) { - return mapping->uri; - } - - /* String comparison only if both non-NULL */ - if (prefix && mapping->prefix && strcmp(prefix, mapping->prefix) == 0) { - return mapping->uri; - } - } - - return NULL; /* Prefix not found */ -} - -/* Helper: Collect namespaces from element and all descendants recursively */ -static void collect_namespaces_recursive(XPathContext* context, - struct taurus_element* element) { - if (!element) return; - - /* Register namespaces from this element's namespace declarations - * Note: namespaces is a linked list, not an array */ - struct taurus_namespace* ns = element->namespaces; - while (ns) { - if (ns->uri) { - xpath_context_register_namespace(context, ns->prefix, ns->uri); - } - ns = ns->next; - } - - /* Also check attributes for xmlns declarations - * (in case they weren't already parsed into namespaces list) */ - for (size_t i = 0; i < element->attributes_count; i++) { - struct taurus_attribute* attr = element->attributes[i]; - if (!attr || !attr->name) continue; - - /* Check for xmlns:prefix or xmlns */ - if (strncmp(attr->name, "xmlns:", 6) == 0) { - /* xmlns:prefix="uri" */ - xpath_context_register_namespace(context, attr->name + 6, attr->value); - } else if (strcmp(attr->name, "xmlns") == 0) { - /* xmlns="uri" (default namespace) */ - xpath_context_register_namespace(context, NULL, attr->value); - } - } - - /* Recursively collect from children */ - for (size_t i = 0; i < element->children_count; i++) { - collect_namespaces_recursive(context, element->children[i]); - } -} - -/** - * Auto-populate namespace mappings from entire document tree - * - * @param context XPath context - */ -void xpath_context_init_from_document(XPathContext* context) { - if (!context || !context->document || !context->document->root) return; - - /* Collect namespaces from entire document tree - * This ensures namespace declarations on any element are available */ - collect_namespaces_recursive(context, context->document->root); -} - -/* ============================================================================ - * NodeSet Management - * ============================================================================ */ - -XPathNodeSet* xpath_nodeset_new(void) { - return xpath_nodeset_new_with_capacity(0); -} - -XPathNodeSet* xpath_nodeset_new_with_capacity(size_t capacity) { - XPathNodeSet* nodeset = TAURUS_ALLOC(XPathNodeSet); - if (!nodeset) return NULL; - - if (capacity > 0) { - nodeset->nodes = (void**)TAURUS_ALLOC_N(void*, capacity); - if (!nodeset->nodes) { - TAURUS_FREE(nodeset); - return NULL; - } - nodeset->capacity = capacity; - } else { - nodeset->nodes = NULL; - nodeset->capacity = 0; - } - - nodeset->count = 0; - nodeset->owns_attributes = 1; /* By default, nodeset owns its attribute nodes */ - return nodeset; -} - -void xpath_nodeset_free(XPathNodeSet* nodeset) { - if (!nodeset) return; - - if (nodeset->nodes && nodeset->owns_attributes) { - /* Free typed nodes only if we own them - check node type and free appropriately */ - for (size_t i = 0; i < nodeset->count; i++) { - void* node = nodeset->nodes[i]; - if (!node) continue; - - TaurusNodeType node_type = XPATH_NODE_TYPE(node); - if (node_type == TAURUS_NODE_ATTRIBUTE) { - /* Attribute node - free its allocated memory */ - TaurusAttributeNode* attr_node = (TaurusAttributeNode*)node; - if (attr_node->name) TAURUS_FREE(attr_node->name); - if (attr_node->value) TAURUS_FREE(attr_node->value); - if (attr_node->namespace_uri) TAURUS_FREE(attr_node->namespace_uri); - TAURUS_FREE(attr_node); - } - /* Element nodes are owned by document, don't free */ - } - } - if (nodeset->nodes) { - TAURUS_FREE(nodeset->nodes); - } - TAURUS_FREE(nodeset); -} - -size_t xpath_nodeset_count(XPathNodeSet* nodeset) { - return nodeset ? nodeset->count : 0; -} - -void* xpath_nodeset_get(XPathNodeSet* nodeset, size_t index) { - if (!nodeset || !nodeset->nodes || index >= nodeset->count) { - return NULL; - } - return nodeset->nodes[index]; -} - -void xpath_nodeset_add(XPathNodeSet* nodeset, void* node) { - if (!nodeset || !node) return; - - /* Resize if needed */ - if (nodeset->count >= nodeset->capacity) { - size_t new_capacity = nodeset->capacity == 0 ? 8 : nodeset->capacity * 2; - void** new_nodes = (void**)TAURUS_REALLOC_N( - nodeset->nodes, void*, new_capacity); - if (!new_nodes) return; - nodeset->nodes = new_nodes; - nodeset->capacity = new_capacity; - } - - nodeset->nodes[nodeset->count++] = node; -} - -/* Helper: Create attribute node from taurus_attribute */ -static TaurusAttributeNode* create_attribute_node(struct taurus_attribute* attr, - struct taurus_element* owner) { - if (!attr) return NULL; - - TaurusAttributeNode* attr_node = TAURUS_ALLOC(TaurusAttributeNode); - if (!attr_node) return NULL; - - attr_node->node_type = TAURUS_NODE_ATTRIBUTE; - attr_node->name = taurus_strdup(attr->name); - attr_node->value = taurus_strdup(attr->value); - attr_node->namespace_uri = attr->namespace_uri ? taurus_strdup(attr->namespace_uri) : NULL; - attr_node->owner = owner; - - return attr_node; -} - -/* Helper: Get element from typed node (returns NULL if not element) - * - * IMPORTANT: Elements are stored as plain taurus_element* without a type field. - * Only attribute nodes have a node_type field as their first member. - * - * Strategy: Check if first field is TAURUS_NODE_ATTRIBUTE. If so, it's an attribute. - * Otherwise, treat it as an element. - */ -static struct taurus_element* node_as_element(void* node) { - if (!node) return NULL; - - /* Check if it's an attribute node by reading first field */ - TaurusNodeType first_field = *(TaurusNodeType*)node; - if (first_field == TAURUS_NODE_ATTRIBUTE) { - /* It's an attribute node, not an element */ - return NULL; - } - - /* Otherwise, it's an element (no type field in taurus_element) */ - return (struct taurus_element*)node; -} - -/* Helper: Get attribute node from typed node (returns NULL if not attribute) */ -static TaurusAttributeNode* node_as_attribute(void* node) { - if (!node) return NULL; - - /* Check if first field is TAURUS_NODE_ATTRIBUTE */ - TaurusNodeType first_field = *(TaurusNodeType*)node; - return (first_field == TAURUS_NODE_ATTRIBUTE) ? (TaurusAttributeNode*)node : NULL; -} - -/* ============================================================================ - * Result Management - * ============================================================================ */ - -struct taurus_xpath_result* xpath_result_new(XPathResultType type) { - struct taurus_xpath_result* result = TAURUS_ALLOC(struct taurus_xpath_result); - if (!result) return NULL; - - result->type = type; - memset(&result->value, 0, sizeof(XPathResultValue)); - return result; -} - -void xpath_result_free(struct taurus_xpath_result* result) { - if (!result) return; - - switch (result->type) { - case XPATH_RESULT_STRING: - if (result->value.string_value) { - TAURUS_FREE(result->value.string_value); - } - break; - case XPATH_RESULT_NODESET: - xpath_nodeset_free(result->value.nodeset_value); - break; - default: - break; - } - - TAURUS_FREE(result); -} - -/* ============================================================================ - * Type Conversions (XPath 1.0 Spec Section 4) - * ============================================================================ */ - -/* Get text content from typed node (handles elements and attributes) */ -static char* get_node_text(void* node) { - if (!node) return taurus_strdup(""); - - TaurusNodeType node_type = XPATH_NODE_TYPE(node); - - switch (node_type) { - case TAURUS_NODE_ELEMENT: { - struct taurus_element* element = (struct taurus_element*)node; - - /* If element has direct text content, return it */ - if (element->text_content) { - return taurus_strdup(element->text_content); - } - - /* Otherwise concatenate all descendant text */ - size_t total_len = 0; - size_t capacity = 256; - char* result = TAURUS_ALLOC_N(char, capacity); - if (!result) return taurus_strdup(""); - result[0] = '\0'; - - /* Recursively collect text from children */ - for (size_t i = 0; i < element->children_count; i++) { - char* child_text = get_node_text(element->children[i]); - if (child_text) { - size_t child_len = strlen(child_text); - if (total_len + child_len + 1 > capacity) { - capacity = (total_len + child_len + 1) * 2; - char* new_result = TAURUS_REALLOC_N(result, char, capacity); - if (!new_result) { - TAURUS_FREE(child_text); - TAURUS_FREE(result); - return taurus_strdup(""); - } - result = new_result; - } - strcat(result, child_text); - total_len += child_len; - TAURUS_FREE(child_text); - } - } - - return result; - } - - case TAURUS_NODE_ATTRIBUTE: { - TaurusAttributeNode* attr_node = (TaurusAttributeNode*)node; - return taurus_strdup(attr_node->value ? attr_node->value : ""); - } - - default: - return taurus_strdup(""); - } -} - -int xpath_to_boolean(struct taurus_xpath_result* result) { - if (!result) return 0; - - switch (result->type) { - case XPATH_RESULT_BOOLEAN: - return result->value.boolean_value; - case XPATH_RESULT_NUMBER: - return result->value.number_value != 0.0 && !isnan(result->value.number_value); - case XPATH_RESULT_STRING: - return result->value.string_value && result->value.string_value[0] != '\0'; - case XPATH_RESULT_NODESET: - return xpath_nodeset_count(result->value.nodeset_value) > 0; - default: - return 0; - } -} - -double xpath_to_number(struct taurus_xpath_result* result) { - if (!result) return NAN; - - switch (result->type) { - case XPATH_RESULT_NUMBER: - return result->value.number_value; - case XPATH_RESULT_BOOLEAN: - return result->value.boolean_value ? 1.0 : 0.0; - case XPATH_RESULT_STRING: { - if (!result->value.string_value) return NAN; - const char* str = result->value.string_value; - - /* Skip leading whitespace */ - while (isspace((unsigned char)*str)) str++; - if (*str == '\0') return NAN; - - /* Parse number */ - char* endptr; - double value = strtod(str, &endptr); - - /* Skip trailing whitespace */ - while (isspace((unsigned char)*endptr)) endptr++; - - /* Must consume entire string */ - return (*endptr == '\0') ? value : NAN; - } - case XPATH_RESULT_NODESET: { - /* Convert first node's string value to number */ - XPathNodeSet* nodeset = result->value.nodeset_value; - if (!nodeset || xpath_nodeset_count(nodeset) == 0) { - return NAN; - } - void* first = xpath_nodeset_get(nodeset, 0); - char* str = get_node_text(first); - if (!str) return NAN; - - /* Parse the string */ - const char* p = str; - while (isspace((unsigned char)*p)) p++; - if (*p == '\0') { - TAURUS_FREE(str); - return NAN; - } - - char* endptr; - double value = strtod(p, &endptr); - while (isspace((unsigned char)*endptr)) endptr++; - - int valid = (*endptr == '\0'); - TAURUS_FREE(str); - return valid ? value : NAN; - } - default: - return NAN; - } -} - -char* xpath_to_string(struct taurus_xpath_result* result) { - if (!result) return taurus_strdup(""); - - switch (result->type) { - case XPATH_RESULT_STRING: - return taurus_strdup(result->value.string_value ? - result->value.string_value : ""); - case XPATH_RESULT_NUMBER: { - char buf[64]; - double num = result->value.number_value; - if (isnan(num)) { - return taurus_strdup("NaN"); - } else if (isinf(num)) { - return taurus_strdup(num > 0 ? "Infinity" : "-Infinity"); - } else { - snprintf(buf, sizeof(buf), "%g", num); - return taurus_strdup(buf); - } - } - case XPATH_RESULT_BOOLEAN: - return taurus_strdup(result->value.boolean_value ? "true" : "false"); - case XPATH_RESULT_NODESET: { - XPathNodeSet* nodeset = result->value.nodeset_value; - if (!nodeset || xpath_nodeset_count(nodeset) == 0) { - return taurus_strdup(""); - } - return get_node_text(xpath_nodeset_get(nodeset, 0)); - } - default: - return taurus_strdup(""); - } -} - -/* ============================================================================ - * Node Test Matching - * ============================================================================ */ - -static int matches_node_test(XPathContext* ctx, struct taurus_element* node, XPathASTNode* test) { - if (!node || !test) return 1; /* No test means match all */ - - switch (test->type) { - case XPATH_AST_NODE_TEST_NAME: - /* Match specific name - namespace-aware (v0.8.0) */ - if (!test->value || !node->name) return 0; - - /* If test has no prefix, match local name only (backward compatible) */ - if (!test->prefix) { - const char* local_name = node->name; - const char* colon = strchr(node->name, ':'); - if (colon) local_name = colon + 1; - - const char* test_name = test->local_name ? test->local_name : test->value; - return strcmp(local_name, test_name) == 0; - } - - /* Test has prefix - need namespace-aware matching */ - /* 1. Resolve test prefix to URI */ - const char* test_uri = xpath_context_resolve_prefix(ctx, test->prefix); - if (!test_uri) return 0; /* Unknown prefix */ - - /* 2. Get element's namespace URI */ - const char* element_uri = node->namespace_uri; - if (!element_uri) return 0; /* Element not in namespace */ - - /* 3. Match URIs */ - if (strcmp(test_uri, element_uri) != 0) return 0; - - /* URIs match - now check local name */ - const char* element_local = node->name; - const char* colon = strchr(node->name, ':'); - if (colon) element_local = colon + 1; - - const char* test_local = test->local_name ? test->local_name : test->value; - return strcmp(element_local, test_local) == 0; - - case XPATH_AST_NODE_TEST_ALL: - /* Wildcard - if test has prefix, match namespace (v0.8.0) */ - if (test->prefix) { - const char* test_uri = xpath_context_resolve_prefix(ctx, test->prefix); - if (!test_uri) return 0; - const char* element_uri = node->namespace_uri; - return element_uri && strcmp(test_uri, element_uri) == 0; - } - /* No prefix - match all elements */ - return 1; - - case XPATH_AST_NODE_TEST_TYPE: - /* Node type tests (node(), text(), etc.) */ - if (test->value) { - if (strcmp(test->value, "node") == 0) { - return 1; /* node() matches all nodes */ - } - /* text(), comment() not fully implemented yet */ - } - return 0; - - default: - return 0; - } -} - -/* ============================================================================ - * Axis Implementations (All 13 XPath Axes) - * ============================================================================ */ - -/* Helper: Collect descendants recursively */ -static void collect_descendants(XPathContext* ctx, - struct taurus_element* node, - XPathNodeSet* result, - XPathASTNode* node_test) { - if (!node) return; - - for (size_t i = 0; i < node->children_count; i++) { - struct taurus_element* child = node->children[i]; - if (matches_node_test(ctx, child, node_test)) { - xpath_nodeset_add(result, child); - } - collect_descendants(ctx, child, result, node_test); - } -} - -/* Helper: Collect descendants or self */ -static void collect_descendants_or_self(XPathContext* ctx, - struct taurus_element* node, - XPathNodeSet* result, - XPathASTNode* node_test) { - if (!node) return; - - if (matches_node_test(ctx, node, node_test)) { - xpath_nodeset_add(result, node); - } - - for (size_t i = 0; i < node->children_count; i++) { - collect_descendants_or_self(ctx, node->children[i], result, node_test); - } -} - -/* child:: axis */ -static XPathNodeSet* axis_child(XPathContext* ctx, struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node) return result; - - for (size_t i = 0; i < node->children_count; i++) { - struct taurus_element* child = node->children[i]; - if (matches_node_test(ctx, child, test)) { - xpath_nodeset_add(result, child); - } - } - - return result; -} - -/* descendant:: axis */ -static XPathNodeSet* axis_descendant(XPathContext* ctx, struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node) return result; - collect_descendants(ctx, node, result, test); - return result; -} - -/* descendant-or-self:: axis */ -static XPathNodeSet* axis_descendant_or_self(XPathContext* ctx, - struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node) return result; - collect_descendants_or_self(ctx, node, result, test); - return result; -} - -/* parent:: axis */ -static XPathNodeSet* axis_parent(XPathContext* ctx, struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node || !node->parent) return result; - - if (matches_node_test(ctx, node->parent, test)) { - xpath_nodeset_add(result, node->parent); - } - - return result; -} - -/* ancestor:: axis */ -static XPathNodeSet* axis_ancestor(XPathContext* ctx, struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node) return result; - - struct taurus_element* current = node->parent; - while (current) { - if (matches_node_test(ctx, current, test)) { - xpath_nodeset_add(result, current); - } - current = current->parent; - } - - return result; -} - -/* ancestor-or-self:: axis */ -static XPathNodeSet* axis_ancestor_or_self(XPathContext* ctx, - struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node) return result; - - if (matches_node_test(ctx, node, test)) { - xpath_nodeset_add(result, node); - } - - struct taurus_element* current = node->parent; - while (current) { - if (matches_node_test(ctx, current, test)) { - xpath_nodeset_add(result, current); - } - current = current->parent; - } - - return result; -} - -/* self:: axis */ -static XPathNodeSet* axis_self(XPathContext* ctx, struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node) return result; - - if (matches_node_test(ctx, node, test)) { - xpath_nodeset_add(result, node); - } - - return result; -} - -/* following-sibling:: axis */ -static XPathNodeSet* axis_following_sibling(XPathContext* ctx, - struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node || !node->parent) return result; - - struct taurus_element* parent = node->parent; - int found = 0; - - for (size_t i = 0; i < parent->children_count; i++) { - if (parent->children[i] == node) { - found = 1; - continue; - } - if (found && matches_node_test(ctx, parent->children[i], test)) { - xpath_nodeset_add(result, parent->children[i]); - } - } - - return result; -} - -/* preceding-sibling:: axis */ -static XPathNodeSet* axis_preceding_sibling(XPathContext* ctx, - struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node || !node->parent) return result; - - struct taurus_element* parent = node->parent; - - for (size_t i = 0; i < parent->children_count; i++) { - if (parent->children[i] == node) break; - if (matches_node_test(ctx, parent->children[i], test)) { - xpath_nodeset_add(result, parent->children[i]); - } - } - - return result; -} - -/* following:: axis - all nodes after context in document order */ -static XPathNodeSet* axis_following(XPathContext* ctx, struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node || !node->parent) return result; - - /* Get following siblings and their descendants */ - struct taurus_element* parent = node->parent; - int found = 0; - - for (size_t i = 0; i < parent->children_count; i++) { - if (parent->children[i] == node) { - found = 1; - continue; - } - if (found) { - if (matches_node_test(ctx, parent->children[i], test)) { - xpath_nodeset_add(result, parent->children[i]); - } - collect_descendants(ctx, parent->children[i], result, test); - } - } - - /* Recursively get parent's following nodes */ - XPathNodeSet* parent_following = axis_following(ctx, parent, test); - if (parent_following) { - for (size_t i = 0; i < xpath_nodeset_count(parent_following); i++) { - xpath_nodeset_add(result, xpath_nodeset_get(parent_following, i)); - } - xpath_nodeset_free(parent_following); - } - - return result; -} - -/* preceding:: axis - all nodes before context in document order */ -static XPathNodeSet* axis_preceding(XPathContext* ctx, struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node || !node->parent) return result; - - /* Get preceding siblings and their descendants */ - struct taurus_element* parent = node->parent; - - for (size_t i = 0; i < parent->children_count; i++) { - if (parent->children[i] == node) break; - if (matches_node_test(ctx, parent->children[i], test)) { - xpath_nodeset_add(result, parent->children[i]); - } - collect_descendants(ctx, parent->children[i], result, test); - } - - /* Recursively get parent's preceding nodes */ - XPathNodeSet* parent_preceding = axis_preceding(ctx, parent, test); - if (parent_preceding) { - for (size_t i = 0; i < xpath_nodeset_count(parent_preceding); i++) { - xpath_nodeset_add(result, xpath_nodeset_get(parent_preceding, i)); - } - xpath_nodeset_free(parent_preceding); - } - - return result; -} - -/* attribute:: axis - * - * Returns attributes as proper TaurusAttributeNode structures. - * These nodes have type TAURUS_NODE_ATTRIBUTE and work with all - * type conversion functions properly. - */ -static XPathNodeSet* axis_attribute(XPathContext* ctx, struct taurus_element* node, - XPathASTNode* test) { - DEBUG_LOG(" === axis_attribute START ==="); - DEBUG_LOG(" node=%p, name=%s", (void*)node, node ? node->name : "(null)"); - DEBUG_LOG(" attributes_count=%zu", node ? (size_t)node->attributes_count : 0); - - XPathNodeSet* result = xpath_nodeset_new(); - if (!result || !node) { - DEBUG_LOG(" EARLY RETURN: result=%p, node=%p", (void*)result, (void*)node); - return result; - } - - /* Iterate through element's attributes */ - for (size_t i = 0; i < node->attributes_count; i++) { - struct taurus_attribute* attr = node->attributes[i]; - DEBUG_LOG(" [%zu] attr=%p", i, (void*)attr); - if (!attr) { - DEBUG_LOG(" [%zu] SKIPPED: attr is NULL", i); - continue; - } - DEBUG_LOG(" [%zu] attr->name=%s, attr->value=%s", - i, attr->name ? attr->name : "(null)", - attr->value ? attr->value : "(null)"); - - /* Check if attribute matches node test */ - int matches = 0; - if (test && test->type == XPATH_AST_NODE_TEST_NAME) { - /* Specific attribute name test */ - matches = (test->value && attr->name && strcmp(test->value, attr->name) == 0); - DEBUG_LOG(" [%zu] NAME test: looking for '%s', matches=%d", - i, test->value ? test->value : "(null)", matches); - } else if (test && test->type == XPATH_AST_NODE_TEST_ALL) { - /* Wildcard - matches all attributes */ - matches = 1; - DEBUG_LOG(" [%zu] WILDCARD test: matches=%d", i, matches); - } else if (!test) { - /* No test means match all */ - matches = 1; - DEBUG_LOG(" [%zu] NO test: matches=%d", i, matches); - } - - if (matches) { - /* Create proper attribute node */ - DEBUG_LOG(" [%zu] Creating attribute node...", i); - TaurusAttributeNode* attr_node = create_attribute_node(attr, node); - DEBUG_LOG(" [%zu] attr_node=%p", i, (void*)attr_node); - if (attr_node) { - DEBUG_LOG(" [%zu] attr_node->node_type=%d (should be 1)", - i, (int)attr_node->node_type); - DEBUG_LOG(" [%zu] attr_node->name=%s", - i, attr_node->name ? attr_node->name : "(null)"); - DEBUG_LOG(" [%zu] attr_node->value=%s", - i, attr_node->value ? attr_node->value : "(null)"); - DEBUG_LOG(" [%zu] Adding to nodeset...", i); - xpath_nodeset_add(result, (void*)attr_node); - DEBUG_LOG(" [%zu] Added. Nodeset count now: %zu", - i, xpath_nodeset_count(result)); - } else { - DEBUG_LOG(" [%zu] FAILED to create attr_node!", i); - } - } - } - - DEBUG_LOG(" Final nodeset count: %zu", xpath_nodeset_count(result)); - DEBUG_LOG(" === axis_attribute END ==="); - return result; -} - -/* namespace:: axis */ -static XPathNodeSet* axis_namespace(XPathContext* ctx, struct taurus_element* node, - XPathASTNode* test) { - XPathNodeSet* result = xpath_nodeset_new(); - /* Namespace axis rarely used, stub for now */ - return result; -} - -/* Apply axis dispatcher */ -static XPathNodeSet* apply_axis(XPathContext* ctx, struct taurus_element* node, - const char* axis_name, XPathASTNode* test) { - DEBUG_LOG(" === apply_axis: %s ===", axis_name ? axis_name : "(null/child)"); - if (!axis_name) { - DEBUG_LOG(" Using default 'child' axis"); - return axis_child(ctx, node, test); - } - - if (strcmp(axis_name, "child") == 0) { - DEBUG_LOG(" Using 'child' axis"); - return axis_child(ctx, node, test); - } - if (strcmp(axis_name, "descendant") == 0) { - DEBUG_LOG(" Using 'descendant' axis"); - return axis_descendant(ctx, node, test); - } - if (strcmp(axis_name, "descendant-or-self") == 0) { - DEBUG_LOG(" Using 'descendant-or-self' axis"); - return axis_descendant_or_self(ctx, node, test); - } - if (strcmp(axis_name, "parent") == 0) return axis_parent(ctx, node, test); - if (strcmp(axis_name, "ancestor") == 0) return axis_ancestor(ctx, node, test); - if (strcmp(axis_name, "ancestor-or-self") == 0) - return axis_ancestor_or_self(ctx, node, test); - if (strcmp(axis_name, "self") == 0) return axis_self(ctx, node, test); - if (strcmp(axis_name, "following-sibling") == 0) - return axis_following_sibling(ctx, node, test); - if (strcmp(axis_name, "preceding-sibling") == 0) - return axis_preceding_sibling(ctx, node, test); - if (strcmp(axis_name, "following") == 0) return axis_following(ctx, node, test); - if (strcmp(axis_name, "preceding") == 0) return axis_preceding(ctx, node, test); - if (strcmp(axis_name, "attribute") == 0) return axis_attribute(ctx, node, test); - if (strcmp(axis_name, "namespace") == 0) return axis_namespace(ctx, node, test); - - return xpath_nodeset_new(); /* Unknown axis */ -} - -/* ============================================================================ - * Operator Evaluation - * ============================================================================ */ - -static struct taurus_xpath_result* evaluate_operator(XPathContext* ctx, - XPathASTNode* ast) { - if (!ast || ast->child_count < 1) return NULL; - - XPathOperatorType op = (XPathOperatorType)ast->number_value; - - /* Unary negation */ - if (op == XPATH_OP_NEGATION) { - struct taurus_xpath_result* operand = evaluate_expr(ctx, ast->children[0]); - if (!operand) return NULL; - double value = xpath_to_number(operand); - xpath_result_free(operand); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER); - if (result) result->value.number_value = -value; - return result; - } - - /* Binary operators require 2 operands */ - if (ast->child_count < 2) return NULL; - - struct taurus_xpath_result* left = evaluate_expr(ctx, ast->children[0]); - if (!left) return NULL; - - /* Short-circuit for logical operators */ - if (op == XPATH_OP_AND) { - if (!xpath_to_boolean(left)) { - xpath_result_free(left); - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_BOOLEAN); - if (result) result->value.boolean_value = 0; - return result; - } - } else if (op == XPATH_OP_OR) { - if (xpath_to_boolean(left)) { - xpath_result_free(left); - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_BOOLEAN); - if (result) result->value.boolean_value = 1; - return result; - } - } - - struct taurus_xpath_result* right = evaluate_expr(ctx, ast->children[1]); - if (!right) { - xpath_result_free(left); - return NULL; - } - - struct taurus_xpath_result* result = NULL; - - /* Arithmetic operators */ - if (op == XPATH_OP_PLUS || op == XPATH_OP_MINUS || op == XPATH_OP_MULTIPLY || - op == XPATH_OP_DIV || op == XPATH_OP_MOD) { - double lval = xpath_to_number(left); - double rval = xpath_to_number(right); - result = xpath_result_new(XPATH_RESULT_NUMBER); - if (result) { - switch (op) { - case XPATH_OP_PLUS: result->value.number_value = lval + rval; break; - case XPATH_OP_MINUS: result->value.number_value = lval - rval; break; - case XPATH_OP_MULTIPLY: result->value.number_value = lval * rval; break; - case XPATH_OP_DIV: result->value.number_value = lval / rval; break; - case XPATH_OP_MOD: result->value.number_value = fmod(lval, rval); break; - default: break; - } - } - } - /* Comparison operators */ - else if (op >= XPATH_OP_EQUAL && op <= XPATH_OP_GREATER_EQUAL) { - result = xpath_result_new(XPATH_RESULT_BOOLEAN); - if (result) { - /* XPath spec: if both operands are strings, compare as strings */ - if (left->type == XPATH_RESULT_STRING && right->type == XPATH_RESULT_STRING) { - /* String comparison */ - const char* lstr = left->value.string_value ? left->value.string_value : ""; - const char* rstr = right->value.string_value ? right->value.string_value : ""; - int cmp = strcmp(lstr, rstr); - - switch (op) { - case XPATH_OP_EQUAL: result->value.boolean_value = (cmp == 0); break; - case XPATH_OP_NOT_EQUAL: result->value.boolean_value = (cmp != 0); break; - case XPATH_OP_LESS: result->value.boolean_value = (cmp < 0); break; - case XPATH_OP_LESS_EQUAL: result->value.boolean_value = (cmp <= 0); break; - case XPATH_OP_GREATER: result->value.boolean_value = (cmp > 0); break; - case XPATH_OP_GREATER_EQUAL: result->value.boolean_value = (cmp >= 0); break; - default: break; - } - } else { - /* Numeric comparison (default for mixed types) */ - double lval = xpath_to_number(left); - double rval = xpath_to_number(right); - switch (op) { - case XPATH_OP_EQUAL: result->value.boolean_value = (lval == rval); break; - case XPATH_OP_NOT_EQUAL: result->value.boolean_value = (lval != rval); break; - case XPATH_OP_LESS: result->value.boolean_value = (lval < rval); break; - case XPATH_OP_LESS_EQUAL: result->value.boolean_value = (lval <= rval); break; - case XPATH_OP_GREATER: result->value.boolean_value = (lval > rval); break; - case XPATH_OP_GREATER_EQUAL: result->value.boolean_value = (lval >= rval); break; - default: break; - } - } - } - } - /* Logical operators */ - else if (op == XPATH_OP_AND || op == XPATH_OP_OR) { - int lbool = xpath_to_boolean(left); - int rbool = xpath_to_boolean(right); - result = xpath_result_new(XPATH_RESULT_BOOLEAN); - if (result) { - result->value.boolean_value = (op == XPATH_OP_AND) ? (lbool && rbool) : (lbool || rbool); - } - } - /* Union operator */ - else if (op == XPATH_OP_UNION) { - if (left->type != XPATH_RESULT_NODESET || right->type != XPATH_RESULT_NODESET) { - xpath_result_free(left); - xpath_result_free(right); - return NULL; - } - result = xpath_result_new(XPATH_RESULT_NODESET); - if (result) { - XPathNodeSet* ns = xpath_nodeset_new(); - /* Add left nodes */ - for (size_t i = 0; i < xpath_nodeset_count(left->value.nodeset_value); i++) { - xpath_nodeset_add(ns, xpath_nodeset_get(left->value.nodeset_value, i)); - } - /* Add right nodes (skip duplicates) */ - for (size_t i = 0; i < xpath_nodeset_count(right->value.nodeset_value); i++) { - struct taurus_element* node = xpath_nodeset_get(right->value.nodeset_value, i); - int duplicate = 0; - for (size_t j = 0; j < xpath_nodeset_count(ns); j++) { - if (xpath_nodeset_get(ns, j) == node) { - duplicate = 1; - break; - } - } - if (!duplicate) xpath_nodeset_add(ns, node); - } - result->value.nodeset_value = ns; - } - } - - xpath_result_free(left); - xpath_result_free(right); - return result; -} - -/* ============================================================================ - * Predicate Evaluation - * ============================================================================ */ - -static XPathNodeSet* apply_predicates(XPathContext* ctx, XPathNodeSet* nodes, - XPathASTNode** predicates, size_t pred_count) { - if (!nodes || pred_count == 0) return nodes; - - DEBUG_LOG(" === apply_predicates: pred_count=%zu, nodeset size=%zu ===", - pred_count, xpath_nodeset_count(nodes)); - - XPathNodeSet* result = nodes; - - for (size_t p = 0; p < pred_count; p++) { - DEBUG_LOG(" Processing predicate %zu", p); - XPathNodeSet* filtered = xpath_nodeset_new(); - if (!filtered) { - DEBUG_LOG(" FAILED to create filtered nodeset"); - break; - } - - size_t size = xpath_nodeset_count(result); - DEBUG_LOG(" Filtering %zu nodes", size); - - for (size_t i = 0; i < size; i++) { - void* node = xpath_nodeset_get(result, i); - - /* For predicates, we need element context - attributes predicate on their owner */ - struct taurus_element* context_elem = node_as_element(node); - if (!context_elem) { - TaurusAttributeNode* attr_node = node_as_attribute(node); - if (attr_node) { - context_elem = attr_node->owner; - } - } - - if (!context_elem) { - DEBUG_LOG(" Node[%zu]: No valid context element, skipping", i); - continue; /* Skip if no valid context */ - } - - DEBUG_LOG(" Node[%zu]: context_elem=%p (name=%s)", - i, (void*)context_elem, context_elem->name ? context_elem->name : "(null)"); - - /* Save context */ - struct taurus_element* old_node = ctx->context_node; - size_t old_pos = ctx->context_position; - size_t old_size = ctx->context_size; - - ctx->context_node = context_elem; - ctx->context_position = i + 1; /* 1-based */ - ctx->context_size = size; - - DEBUG_LOG(" Evaluating predicate with context pos=%zu, size=%zu", - ctx->context_position, ctx->context_size); - - /* Evaluate predicate */ - struct taurus_xpath_result* pred_result = evaluate_expr(ctx, predicates[p]); - - /* Restore context */ - ctx->context_node = old_node; - ctx->context_position = old_pos; - ctx->context_size = old_size; - - if (pred_result) { - DEBUG_LOG(" Predicate result type=%d", pred_result->type); - /* Numeric predicate: matches position */ - if (pred_result->type == XPATH_RESULT_NUMBER) { - DEBUG_LOG(" Number=%f, position=%zu", - pred_result->value.number_value, i + 1); - if ((size_t)pred_result->value.number_value == i + 1) { - DEBUG_LOG(" MATCH! Adding node"); - xpath_nodeset_add(filtered, node); - } - } - /* Boolean predicate */ - else if (xpath_to_boolean(pred_result)) { - DEBUG_LOG(" Boolean=true, adding node"); - xpath_nodeset_add(filtered, node); - } - xpath_result_free(pred_result); - } else { - DEBUG_LOG(" Predicate evaluation returned NULL! Error: %s", - ctx->error_msg[0] ? ctx->error_msg : "(no error)"); - } - } - - DEBUG_LOG(" Filtered nodeset size: %zu", xpath_nodeset_count(filtered)); - if (result != nodes) xpath_nodeset_free(result); - result = filtered; - } - - DEBUG_LOG(" === apply_predicates END: result size=%zu ===", - xpath_nodeset_count(result)); - return result; -} - -/* ============================================================================ - * Path Expression Evaluation - * ============================================================================ */ - -static struct taurus_xpath_result* evaluate_step(XPathContext* ctx, - XPathASTNode* step, - XPathNodeSet* input) { - DEBUG_LOG(" === evaluate_step START ==="); - if (!step || step->type != XPATH_AST_STEP || !input) { - DEBUG_LOG(" Invalid parameters: step=%p, type=%d, input=%p", - (void*)step, step ? step->type : -1, (void*)input); - return NULL; - } - - const char* axis_name = step->value ? step->value : "child"; - XPathASTNode* node_test = (step->child_count > 0) ? step->children[0] : NULL; - - DEBUG_LOG(" axis_name = %s", axis_name); - DEBUG_LOG(" node_test = %p (type=%d)", (void*)node_test, node_test ? node_test->type : -1); - if (node_test && node_test->value) { - DEBUG_LOG(" node_test->value = %s", node_test->value); - } - DEBUG_LOG(" input nodeset count = %zu", xpath_nodeset_count(input)); - - XPathNodeSet* result = xpath_nodeset_new(); - if (!result) { - DEBUG_LOG(" FAILED to create result nodeset"); - return NULL; - } - - /* Apply axis to each input node (must be elements) */ - for (size_t i = 0; i < xpath_nodeset_count(input); i++) { - void* node_ptr = xpath_nodeset_get(input, i); - struct taurus_element* node = node_as_element(node_ptr); - DEBUG_LOG(" Processing input[%zu]: node=%p", i, (void*)node); - if (!node) { - DEBUG_LOG(" Skipping non-element node"); - continue; /* Skip non-element nodes */ - } - DEBUG_LOG(" node->name = %s, children_count = %zu", - node->name ? node->name : "(null)", node->children_count); - - XPathNodeSet* axis_result = apply_axis(ctx, node, axis_name, node_test); - DEBUG_LOG(" axis_result count = %zu", axis_result ? xpath_nodeset_count(axis_result) : 0); - - if (axis_result) { - /* Apply predicates if present */ - XPathNodeSet* filtered = axis_result; - if (step->child_count > 1) { - filtered = apply_predicates(ctx, axis_result, - &step->children[1], - step->child_count - 1); - } - - /* Transfer nodes to result (result will own the attribute nodes) */ - for (size_t j = 0; j < xpath_nodeset_count(filtered); j++) { - xpath_nodeset_add(result, xpath_nodeset_get(filtered, j)); - } - - /* Don't free attribute nodes from intermediate node sets - result now owns them */ - if (filtered != axis_result) { - filtered->owns_attributes = 0; - xpath_nodeset_free(filtered); - } - axis_result->owns_attributes = 0; /* Result nodeset now owns the attributes */ - xpath_nodeset_free(axis_result); - } - } - - DEBUG_LOG(" Final result count = %zu", xpath_nodeset_count(result)); - DEBUG_LOG(" === evaluate_step END ==="); - - struct taurus_xpath_result* res = xpath_result_new(XPATH_RESULT_NODESET); - if (res) res->value.nodeset_value = result; - return res; -} - -static struct taurus_xpath_result* evaluate_location_path(XPathContext* ctx, - XPathASTNode* path) { - XPathNodeSet* current = xpath_nodeset_new(); - if (!current) return NULL; - - /* Starting nodeset */ - if (path->type == XPATH_AST_ABSOLUTE_PATH) { - /* Special case: Absolute path with element name as first step - * XPath "/root" means "child of document node named root" - * Since we don't have a document node, check if root matches and use it */ - int is_root_match = 0; - struct taurus_element* root = ctx->document->root; - - DEBUG_LOG(" Checking for special case: child_count=%zu, root=%p", - (size_t)path->child_count, (void*)root); - - if (path->child_count > 0 && root && root->name) { - XPathASTNode* first_child = path->children[0]; - - DEBUG_LOG(" First child type=%d (RELATIVE_PATH=%d, STEP=%d)", - first_child->type, XPATH_AST_RELATIVE_PATH, XPATH_AST_STEP); - - /* The first child might be RELATIVE_PATH containing steps, or a direct STEP */ - XPathASTNode* first_step = NULL; - if (first_child->type == XPATH_AST_RELATIVE_PATH && first_child->child_count > 0) { - first_step = first_child->children[0]; - DEBUG_LOG(" Found RELATIVE_PATH, extracting first step"); - } else if (first_child->type == XPATH_AST_STEP) { - first_step = first_child; - DEBUG_LOG(" Found direct STEP"); - } - - /* Check if first step is a simple child axis with element name */ - if (first_step && first_step->type == XPATH_AST_STEP) { - const char* axis = first_step->value ? first_step->value : "child"; - DEBUG_LOG(" Axis=%s, child_count=%zu", axis, (size_t)first_step->child_count); - - if (strcmp(axis, "child") == 0 && first_step->child_count > 0) { - XPathASTNode* node_test = first_step->children[0]; - DEBUG_LOG(" Node test type=%d, value=%s", - node_test->type, node_test->value ? node_test->value : "(null)"); - - if (node_test->type == XPATH_AST_NODE_TEST_NAME && node_test->value) { - /* Extract local name from root (strip namespace prefix if present) */ - const char* root_local = root->name; - const char* colon = strchr(root->name, ':'); - if (colon) root_local = colon + 1; - - DEBUG_LOG(" Comparing root_local='%s' with node_test='%s'", - root_local, node_test->value); - - /* Check if root element name matches */ - if (strcmp(root_local, node_test->value) == 0) { - is_root_match = 1; - DEBUG_LOG(" ✓ Special case: /root matches document root"); - } - } - } - } - } - - DEBUG_LOG(" is_root_match=%d", is_root_match); - - if (is_root_match) { - /* Root matches - add it and process remaining steps - * Structure can be: - * /root/child: ABSOLUTE_PATH → RELATIVE_PATH → [STEP:child::root, STEP:child::child] - * /root: ABSOLUTE_PATH → RELATIVE_PATH → [STEP:child::root] - */ - xpath_nodeset_add(current, root); - DEBUG_LOG(" Added root to nodeset, processing remaining steps"); - - /* Get the RELATIVE_PATH (first child of ABSOLUTE_PATH) */ - XPathASTNode* rel_path = path->children[0]; - if (rel_path && rel_path->type == XPATH_AST_RELATIVE_PATH && rel_path->child_count > 1) { - /* Process steps starting from index 1 (skip first step which matched root) */ - for (size_t j = 1; j < rel_path->child_count; j++) { - XPathASTNode* step = rel_path->children[j]; - if (step->type == XPATH_AST_STEP) { - DEBUG_LOG(" Processing remaining step %zu", j); - struct taurus_xpath_result* step_result = evaluate_step(ctx, step, current); - if (!step_result) { - xpath_nodeset_free(current); - return NULL; - } - xpath_nodeset_free(current); - current = step_result->value.nodeset_value; - step_result->value.nodeset_value = NULL; - xpath_result_free(step_result); - } - } - } - /* If rel_path has only 1 child (the step that matched), we're done - just return root */ - } else { - /* Normal absolute path - start from root and process ALL steps */ - DEBUG_LOG(" Adding root to initial nodeset for absolute path"); - xpath_nodeset_add(current, ctx->document->root); - DEBUG_LOG(" Nodeset count after adding root: %zu", xpath_nodeset_count(current)); - - /* Process steps - handle both direct steps and those in RELATIVE_PATH */ - DEBUG_LOG(" Processing %zu children", (size_t)path->child_count); - for (size_t i = 0; i < path->child_count; i++) { - XPathASTNode* child = path->children[i]; - DEBUG_LOG(" Child[%zu]: type=%d", i, child->type); - - if (child->type == XPATH_AST_STEP) { - DEBUG_LOG(" Processing STEP child"); - DEBUG_LOG(" Input nodeset count: %zu", xpath_nodeset_count(current)); - /* Direct step child - process it */ - struct taurus_xpath_result* step_result = evaluate_step(ctx, child, current); - if (!step_result) { - DEBUG_LOG(" STEP evaluation FAILED"); - xpath_nodeset_free(current); - return NULL; - } - DEBUG_LOG(" STEP result nodeset count: %zu", - xpath_nodeset_count(step_result->value.nodeset_value)); - - xpath_nodeset_free(current); - current = step_result->value.nodeset_value; - step_result->value.nodeset_value = NULL; - xpath_result_free(step_result); - DEBUG_LOG(" Current nodeset count: %zu", xpath_nodeset_count(current)); - } - else if (child->type == XPATH_AST_RELATIVE_PATH) { - /* RELATIVE_PATH container - process its step children */ - for (size_t j = 0; j < child->child_count; j++) { - XPathASTNode* step = child->children[j]; - - if (step->type == XPATH_AST_STEP) { - struct taurus_xpath_result* step_result = evaluate_step(ctx, step, current); - if (!step_result) { - xpath_nodeset_free(current); - return NULL; - } - - xpath_nodeset_free(current); - current = step_result->value.nodeset_value; - step_result->value.nodeset_value = NULL; - xpath_result_free(step_result); - } - } - } - } - } - } /* End of if (path->type == XPATH_AST_ABSOLUTE_PATH) */ - else { - /* Relative path - start from context node */ - xpath_nodeset_add(current, ctx->context_node); - - /* Process steps - handle both direct steps and those in RELATIVE_PATH */ - for (size_t i = 0; i < path->child_count; i++) { - XPathASTNode* child = path->children[i]; - - if (child->type == XPATH_AST_STEP) { - /* Direct step child - process it */ - struct taurus_xpath_result* step_result = evaluate_step(ctx, child, current); - if (!step_result) { - xpath_nodeset_free(current); - return NULL; - } - - xpath_nodeset_free(current); - current = step_result->value.nodeset_value; - step_result->value.nodeset_value = NULL; - xpath_result_free(step_result); - } - else if (child->type == XPATH_AST_RELATIVE_PATH) { - /* RELATIVE_PATH container - process its step children */ - for (size_t j = 0; j < child->child_count; j++) { - XPathASTNode* step = child->children[j]; - - if (step->type == XPATH_AST_STEP) { - struct taurus_xpath_result* step_result = evaluate_step(ctx, step, current); - if (!step_result) { - xpath_nodeset_free(current); - return NULL; - } - - xpath_nodeset_free(current); - current = step_result->value.nodeset_value; - step_result->value.nodeset_value = NULL; - xpath_result_free(step_result); - } - } - } - } - } - - DEBUG_LOG(" Final nodeset count: %zu", xpath_nodeset_count(current)); - DEBUG_LOG("=== evaluate_location_path END ==="); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NODESET); - if (result) result->value.nodeset_value = current; - return result; -} - -/* ============================================================================ - * Main Evaluation Dispatcher - * ============================================================================ */ - -/* Forward declaration for function call evaluation */ -static struct taurus_xpath_result* evaluate_function_call(XPathContext* ctx, - XPathASTNode* ast); - -static struct taurus_xpath_result* evaluate_expr(XPathContext* ctx, XPathASTNode* ast) { - if (!ctx || !ast) return NULL; - - switch (ast->type) { - case XPATH_AST_NUMBER: { - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER); - if (result) result->value.number_value = ast->number_value; - return result; - } - - case XPATH_AST_STRING: { - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (result) result->value.string_value = taurus_strdup(ast->value ? ast->value : ""); - return result; - } - - case XPATH_AST_OPERATOR: - return evaluate_operator(ctx, ast); - - case XPATH_AST_FUNCTION_CALL: - return evaluate_function_call(ctx, ast); - - case XPATH_AST_PATH_EXPR: - case XPATH_AST_ABSOLUTE_PATH: - case XPATH_AST_RELATIVE_PATH: - return evaluate_location_path(ctx, ast); - - case XPATH_AST_STEP: { - XPathNodeSet* initial = xpath_nodeset_new(); - if (initial) xpath_nodeset_add(initial, ctx->context_node); - struct taurus_xpath_result* result = evaluate_step(ctx, ast, initial); - xpath_nodeset_free(initial); - return result; - } - - default: { - char msg[256]; - snprintf(msg, sizeof(msg), "Unsupported AST node type: %d", ast->type); - snprintf(ctx->error_msg, sizeof(ctx->error_msg), "%s", msg); - - if (ctx->input) { - taurus_set_error_with_context( - TAURUS_ERROR_EVAL_CONTEXT, - msg, - ctx->input, - 0, /* No specific position for AST node type errors */ - 1, 1 - ); - } - return NULL; - } - } -} - -/* ============================================================================ - * Function Call Evaluation - * ============================================================================ */ - -/* Helper: Find similar function name (simple Levenshtein distance check) */ -static const char* suggest_similar_function(const char* name) { - if (!name) return NULL; - - /* All XPath 1.0 functions */ - static const char* functions[] = { - "last", "position", "count", "id", "local-name", "namespace-uri", "name", - "string", "concat", "starts-with", "contains", "substring-before", - "substring-after", "substring", "string-length", "normalize-space", "translate", - "boolean", "not", "true", "false", "lang", - "number", "sum", "floor", "ceiling", "round", - NULL - }; - - size_t name_len = strlen(name); - const char* best_match = NULL; - int best_distance = 999; - - for (int i = 0; functions[i]; i++) { - const char* func = functions[i]; - size_t func_len = strlen(func); - - /* Quick filters */ - if (name_len == 0 || func_len == 0) continue; - - /* Check prefix match */ - if (strncmp(name, func, name_len < func_len ? name_len : func_len) == 0) { - return func; /* Strong prefix match */ - } - - /* Simple distance: count character differences */ - int distance = abs((int)name_len - (int)func_len); - for (size_t j = 0; j < (name_len < func_len ? name_len : func_len); j++) { - if (name[j] != func[j]) distance++; - } - - /* Only suggest if reasonably close (within 3 edits) */ - if (distance < best_distance && distance <= 3) { - best_distance = distance; - best_match = func; - } - } - - return best_match; -} - -static struct taurus_xpath_result* evaluate_function_call(XPathContext* ctx, - XPathASTNode* ast) { - if (!ctx || !ast || !ast->value) { - if (ctx) { - char msg[] = "Invalid function call"; - snprintf(ctx->error_msg, sizeof(ctx->error_msg), "%s", msg); - if (ctx->input) { - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_FUNCTION, - msg, ctx->input, 0, 1, 1 - ); - } - } - return NULL; - } - - /* Get function name */ - const char* function_name = ast->value; - - /* Look up function in registry */ - XPathFunctionRegistry* registry = (XPathFunctionRegistry*)ctx->function_registry; - if (!registry) { - char msg[] = "Function registry not initialized"; - snprintf(ctx->error_msg, sizeof(ctx->error_msg), "%s", msg); - if (ctx->input) { - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_FUNCTION, - msg, ctx->input, 0, 1, 1 - ); - } - return NULL; - } - - XPathFunctionDef* func_def = xpath_function_registry_get(registry, function_name); - if (!func_def) { - /* Try to suggest similar function */ - const char* suggestion = suggest_similar_function(function_name); - char msg[256]; - - if (suggestion) { - snprintf(msg, sizeof(msg), - "Unknown function '%s'. Did you mean '%s'?", - function_name, suggestion); - } else { - snprintf(msg, sizeof(msg), - "Unknown function '%s'", function_name); - } - - snprintf(ctx->error_msg, sizeof(ctx->error_msg), "%s", msg); - - if (ctx->input) { - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_FUNCTION, - msg, ctx->input, 0, 1, 1 - ); - } - return NULL; - } - - /* Validate argument count */ - size_t arg_count = ast->child_count; - if ((int)arg_count < func_def->min_args) { - char msg[256]; - snprintf(msg, sizeof(msg), - "Function '%s()' requires at least %d argument%s, got %zu", - function_name, func_def->min_args, - func_def->min_args == 1 ? "" : "s", - arg_count); - snprintf(ctx->error_msg, sizeof(ctx->error_msg), "%s", msg); - - if (ctx->input) { - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_FUNCTION, - msg, ctx->input, 0, 1, 1 - ); - } - return NULL; - } - if (func_def->max_args >= 0 && (int)arg_count > func_def->max_args) { - char msg[256]; - snprintf(msg, sizeof(msg), - "Function '%s()' accepts at most %d argument%s, got %zu", - function_name, func_def->max_args, - func_def->max_args == 1 ? "" : "s", - arg_count); - snprintf(ctx->error_msg, sizeof(ctx->error_msg), "%s", msg); - - if (ctx->input) { - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_FUNCTION, - msg, ctx->input, 0, 1, 1 - ); - } - return NULL; - } - - /* Call function handler */ - return func_def->handler(ctx, ast->children, arg_count); -} - -struct taurus_xpath_result* xpath_evaluate(XPathContext* ctx, XPathASTNode* ast) { - if (!ctx || !ast) return NULL; - return evaluate_expr(ctx, ast); -} - -/* ============================================================================ - * Public XPath Result API (for libtaurus) - * ============================================================================ */ - -/** - * Evaluate XPath with specific context node - */ -TAURUS_API struct taurus_xpath_result* taurus_xpath_eval_with_context( - struct taurus_document* doc, - struct taurus_element* context_node, - const char* xpath_expr, - size_t expr_len -) { - if (!doc || !context_node || !xpath_expr || expr_len == 0) { - return NULL; - } - - /* Parse XPath expression */ - XPathParser* parser = xpath_parser_new(xpath_expr, expr_len); - if (!parser) return NULL; - - XPathASTNode* ast = xpath_parse(parser); - const char* parse_error = xpath_parser_error(parser); - - if (!ast || parse_error) { - xpath_parser_free(parser); - return NULL; - } - - xpath_parser_free(parser); - - /* Create evaluation context with specified context node */ - XPathContext* context = xpath_context_new(doc, context_node); - if (!context) { - ast_node_free(ast); - return NULL; - } - - /* Store input expression for error reporting (v1.0.0) */ - context->input = xpath_expr; - context->input_len = expr_len; - - /* Evaluate expression */ - struct taurus_xpath_result* result = xpath_evaluate(context, ast); - - /* Cleanup */ - xpath_context_free(context); - ast_node_free(ast); - - return result; -} - -/** - * Get result type - */ -TAURUS_API taurus_xpath_result_type taurus_xpath_result_get_type( - const struct taurus_xpath_result* result -) { - if (!result) return TAURUS_XPATH_NODESET; - - /* Convert internal type to public enum */ - switch (result->type) { - case XPATH_RESULT_BOOLEAN: return TAURUS_XPATH_BOOLEAN; - case XPATH_RESULT_NUMBER: return TAURUS_XPATH_NUMBER; - case XPATH_RESULT_STRING: return TAURUS_XPATH_STRING; - case XPATH_RESULT_NODESET: return TAURUS_XPATH_NODESET; - default: return TAURUS_XPATH_NODESET; - } -} - -/** - * Get boolean value from result - */ -TAURUS_API int taurus_xpath_result_as_boolean(const struct taurus_xpath_result* result) { - if (!result) return 0; - - /* Use internal conversion function (it's safe to cast away const) */ - return xpath_to_boolean((struct taurus_xpath_result*)result); -} - -/** - * Get number value from result - */ -TAURUS_API double taurus_xpath_result_as_number(const struct taurus_xpath_result* result) { - if (!result) return NAN; - - /* Use internal conversion function (it's safe to cast away const) */ - return xpath_to_number((struct taurus_xpath_result*)result); -} - -/** - * Get string value from result - */ -TAURUS_API char* taurus_xpath_result_as_string(const struct taurus_xpath_result* result) { - if (!result) return taurus_strdup(""); - - /* Use internal conversion function (it's safe to cast away const) */ - return xpath_to_string((struct taurus_xpath_result*)result); -} - -/** - * Get node-set size - */ -TAURUS_API size_t taurus_xpath_result_nodeset_size( - const struct taurus_xpath_result* result -) { - if (!result || result->type != XPATH_RESULT_NODESET) return 0; - return xpath_nodeset_count(result->value.nodeset_value); -} - -/** - * Get node from node-set (public API - returns elements only) - * - * This function filters the internal nodeset to return only element nodes. - * Attribute nodes are skipped since they cannot be represented as taurus_element*. - * - * Note: The Ruby FFI bridge accesses the nodeset directly and can handle - * attribute nodes correctly by reading their values. - */ -TAURUS_API struct taurus_element* taurus_xpath_result_nodeset_get( - const struct taurus_xpath_result* result, - size_t index -) { - if (!result || result->type != XPATH_RESULT_NODESET) return NULL; - - XPathNodeSet* nodeset = result->value.nodeset_value; - if (!nodeset) return NULL; - - /* Count only element nodes to map index correctly */ - size_t element_index = 0; - for (size_t i = 0; i < nodeset->count; i++) { - void* node = nodeset->nodes[i]; - if (!node) continue; - - TaurusNodeType node_type = XPATH_NODE_TYPE(node); - if (node_type == TAURUS_NODE_ELEMENT) { - if (element_index == index) { - return (struct taurus_element*)node; - } - element_index++; - } - /* Skip attribute nodes - they can't be returned as taurus_element* */ - } - - return NULL; -} \ No newline at end of file diff --git a/ext/taurus/lib/src/xpath/evaluator.h b/ext/taurus/lib/src/xpath/evaluator.h deleted file mode 100644 index da59c30..0000000 --- a/ext/taurus/lib/src/xpath/evaluator.h +++ /dev/null @@ -1,162 +0,0 @@ -/* evaluator.h - XPath evaluator API - * Copyright (c) 2024, Ribose Inc. - * - * Pure C implementation of XPath 1.0 evaluator. - * Provides evaluation of XPath expressions against DOM trees. - */ - -#ifndef XPATH_EVALUATOR_H -#define XPATH_EVALUATOR_H - -#include "../taurus_internal.h" -#include "parser.h" - -/* ============================================================================ - * Types from taurus_internal.h - * ============================================================================ */ - -/* XPathContext, XPathNodeSet, XPathResult are defined in taurus_internal.h */ - -/* ============================================================================ - * Context Management - * ============================================================================ */ - -/** - * Create new XPath evaluation context - * - * @param document Document to evaluate against - * @param context_node Current context node - * @return New context, or NULL on error - */ -XPathContext* xpath_context_new(struct taurus_document* document, - struct taurus_element* context_node); - -/** - * Free XPath context - * - * @param context Context to free - */ -void xpath_context_free(XPathContext* context); - -/** - * Get error message from context - * - * @param context Context to query - * @return Error message, or NULL if no error - */ -const char* xpath_context_error(XPathContext* context); - -/* ============================================================================ - * NodeSet Management - * ============================================================================ */ - -/** - * Create new nodeset - * - * @return New nodeset, or NULL on error - */ -XPathNodeSet* xpath_nodeset_new(void); - -/** - * Create new nodeset with pre-allocated capacity - * - * @param capacity Initial capacity - * @return New nodeset, or NULL on error - */ -XPathNodeSet* xpath_nodeset_new_with_capacity(size_t capacity); - -/** - * Free nodeset - * - * @param nodeset Nodeset to free - */ -void xpath_nodeset_free(XPathNodeSet* nodeset); - -/** - * Get nodeset count - * - * @param nodeset Nodeset to query - * @return Number of nodes in set - */ -size_t xpath_nodeset_count(XPathNodeSet* nodeset); - -/** - * Get node from nodeset (returns typed node pointer) - * - * @param nodeset Nodeset to query - * @param index Index of node (0-based) - * @return Typed node pointer (void*), or NULL if out of bounds - * @note Cast to appropriate type using XPATH_NODE_TYPE macro - */ -void* xpath_nodeset_get(XPathNodeSet* nodeset, size_t index); - -/** - * Add typed node to nodeset - * - * @param nodeset Nodeset to add to - * @param node Typed node pointer to add (element or attribute) - */ -void xpath_nodeset_add(XPathNodeSet* nodeset, void* node); - -/* ============================================================================ - * Result Management - * ============================================================================ */ - -/** - * Create new XPath result - * - * @param type Result type - * @return New result, or NULL on error - */ -struct taurus_xpath_result* xpath_result_new(XPathResultType type); - -/** - * Free XPath result - * - * @param result Result to free - */ -void xpath_result_free(struct taurus_xpath_result* result); - -/* ============================================================================ - * Type Conversions (XPath 1.0 spec section 4) - * ============================================================================ */ - -/** - * Convert result to boolean - * - * @param result Result to convert - * @return Boolean value (0 or 1) - */ -int xpath_to_boolean(struct taurus_xpath_result* result); - -/** - * Convert result to number - * - * @param result Result to convert - * @return Number value (NAN if conversion fails) - */ -double xpath_to_number(struct taurus_xpath_result* result); - -/** - * Convert result to string - * - * @param result Result to convert - * @return String value (caller must free), or NULL on error - */ -char* xpath_to_string(struct taurus_xpath_result* result); - -/* ============================================================================ - * Evaluation - * ============================================================================ */ - -/** - * Evaluate XPath expression - * - * @param context Evaluation context - * @param ast Parsed AST to evaluate - * @return Result of evaluation, or NULL on error - */ -struct taurus_xpath_result* xpath_evaluate(XPathContext* context, - XPathASTNode* ast); - -#endif /* XPATH_EVALUATOR_H */ \ No newline at end of file diff --git a/ext/taurus/lib/src/xpath/functions.c b/ext/taurus/lib/src/xpath/functions.c deleted file mode 100644 index 18511c0..0000000 --- a/ext/taurus/lib/src/xpath/functions.c +++ /dev/null @@ -1,1746 +0,0 @@ -/* functions.c - XPath 1.0 function library implementation - * Copyright (c) 2024, Ribose Inc. - * - * Pure C implementation of all 27 XPath 1.0 standard functions. - * Converted from Ruby C extension to standalone C library. - */ - -#include "functions.h" -#include "evaluator.h" -#include "taurus/taurus.h" -#include -#include -#include -#include -#include -#include -#include - -/* ============================================================================ - * Forward Declarations - * ============================================================================ */ - -/* External evaluator function */ -extern struct taurus_xpath_result* xpath_evaluate(XPathContext* context, - XPathASTNode* ast); - -/* Helper functions */ -static char* get_element_text(struct taurus_element* element); -static char* result_to_string(struct taurus_xpath_result* result); -static int result_to_boolean(struct taurus_xpath_result* result); -static double result_to_number(struct taurus_xpath_result* result); - -/* UTF-8 helpers */ -static size_t utf8_strlen(const char* str); -static size_t utf8_char_offset(const char* str, size_t char_pos); -static char* utf8_substring(const char* str, size_t start_char, size_t char_count); - -/* ============================================================================ - * Function Registry Implementation - * ============================================================================ */ - -XPathFunctionRegistry* xpath_function_registry_new(void) { - XPathFunctionRegistry* registry = TAURUS_ALLOC(XPathFunctionRegistry); - if (!registry) return NULL; - - registry->functions = NULL; - registry->count = 0; - registry->capacity = 0; - - return registry; -} - -void xpath_function_registry_free(XPathFunctionRegistry* registry) { - if (!registry) return; - - if (registry->functions) { - TAURUS_FREE(registry->functions); - } - TAURUS_FREE(registry); -} - -void xpath_function_registry_register( - XPathFunctionRegistry* registry, - const char* name, - XPathFunctionHandler handler, - int min_args, - int max_args -) { - if (!registry || !name || !handler) return; - - /* Resize if needed */ - if (registry->count >= registry->capacity) { - size_t new_capacity = registry->capacity == 0 ? 8 : registry->capacity * 2; - XPathFunctionDef* new_functions = TAURUS_REALLOC_N( - registry->functions, - XPathFunctionDef, - new_capacity - ); - if (!new_functions) return; - registry->functions = new_functions; - registry->capacity = new_capacity; - } - - /* Add function */ - registry->functions[registry->count].name = name; - registry->functions[registry->count].handler = handler; - registry->functions[registry->count].min_args = min_args; - registry->functions[registry->count].max_args = max_args; - registry->count++; -} - -XPathFunctionHandler xpath_function_registry_lookup( - XPathFunctionRegistry* registry, - const char* name -) { - if (!registry || !name) return NULL; - - for (size_t i = 0; i < registry->count; i++) { - if (strcmp(registry->functions[i].name, name) == 0) { - return registry->functions[i].handler; - } - } - - return NULL; -} - -XPathFunctionDef* xpath_function_registry_get( - XPathFunctionRegistry* registry, - const char* name -) { - if (!registry || !name) return NULL; - - for (size_t i = 0; i < registry->count; i++) { - if (strcmp(registry->functions[i].name, name) == 0) { - return ®istry->functions[i]; - } - } - - return NULL; -} - -/* ============================================================================ - * Helper Functions for Type Conversion - * ============================================================================ */ - -/* Get text content from typed node (handles elements and attributes) */ -static char* get_node_text(void* node) { - if (!node) return taurus_strdup(""); - - /* Check if first 4 bytes match TAURUS_NODE_ATTRIBUTE */ - uint32_t first_int = *(uint32_t*)node; - - if (first_int == TAURUS_NODE_ATTRIBUTE) { - /* It's an attribute node - cast to proper type and read value */ - TaurusAttributeNode* attr = (TaurusAttributeNode*)node; - return taurus_strdup(attr->value ? attr->value : ""); - } - - /* It's an element node */ - struct taurus_element* element = (struct taurus_element*)node; - - /* If element has direct text content, return it */ - if (element->text_content) { - return taurus_strdup(element->text_content); - } - - /* Otherwise concatenate all descendant text */ - size_t total_len = 0; - size_t capacity = 256; - char* result = TAURUS_ALLOC_N(char, capacity); - if (!result) return taurus_strdup(""); - result[0] = '\0'; - - /* Recursively collect text from children (children are always elements) */ - for (size_t i = 0; i < element->children_count; i++) { - char* child_text = get_node_text(element->children[i]); - if (child_text) { - size_t child_len = strlen(child_text); - if (total_len + child_len + 1 > capacity) { - capacity = (total_len + child_len + 1) * 2; - char* new_result = TAURUS_REALLOC_N(result, char, capacity); - if (!new_result) { - TAURUS_FREE(child_text); - TAURUS_FREE(result); - return taurus_strdup(""); - } - result = new_result; - } - strcat(result, child_text); - total_len += child_len; - TAURUS_FREE(child_text); - } - } - - return result; -} - -/* Backward compatibility wrapper */ -static char* get_element_text(struct taurus_element* element) { - return get_node_text((void*)element); -} - -/* Convert XPath result to string according to XPath 1.0 spec */ -static char* result_to_string(struct taurus_xpath_result* result) { - if (!result) return taurus_strdup(""); - - switch (result->type) { - case XPATH_RESULT_STRING: - return result->value.string_value ? - taurus_strdup(result->value.string_value) : taurus_strdup(""); - - case XPATH_RESULT_NUMBER: { - double num = result->value.number_value; - char buffer[64]; - - /* Handle special values per XPath spec */ - if (isnan(num)) { - return taurus_strdup("NaN"); - } else if (isinf(num)) { - return taurus_strdup(num > 0 ? "Infinity" : "-Infinity"); - } else if (num == 0.0) { - return taurus_strdup("0"); - } else if (num == floor(num)) { - /* Integer - no decimal point */ - snprintf(buffer, sizeof(buffer), "%.0f", num); - } else { - snprintf(buffer, sizeof(buffer), "%g", num); - } - return taurus_strdup(buffer); - } - - case XPATH_RESULT_BOOLEAN: - return taurus_strdup(result->value.boolean_value ? "true" : "false"); - - case XPATH_RESULT_NODESET: { - /* String value of first node in document order */ - XPathNodeSet* nodeset = result->value.nodeset_value; - if (!nodeset || xpath_nodeset_count(nodeset) == 0) { - return taurus_strdup(""); - } - void* first_node = xpath_nodeset_get(nodeset, 0); - return get_node_text(first_node); - } - - default: - return taurus_strdup(""); - } -} - -/* Convert XPath result to boolean according to XPath 1.0 spec */ -static int result_to_boolean(struct taurus_xpath_result* result) { - if (!result) return 0; - - switch (result->type) { - case XPATH_RESULT_BOOLEAN: - return result->value.boolean_value; - case XPATH_RESULT_NUMBER: - /* Number is false if 0 or NaN */ - return result->value.number_value != 0.0 && - !isnan(result->value.number_value); - case XPATH_RESULT_STRING: - /* String is false if empty */ - return result->value.string_value && - result->value.string_value[0] != '\0'; - case XPATH_RESULT_NODESET: - /* Nodeset is false if empty */ - return xpath_nodeset_count(result->value.nodeset_value) > 0; - default: - return 0; - } -} - -/* Convert XPath result to number according to XPath 1.0 spec */ -static double result_to_number(struct taurus_xpath_result* result) { - if (!result) return NAN; - - switch (result->type) { - case XPATH_RESULT_NUMBER: - return result->value.number_value; - - case XPATH_RESULT_BOOLEAN: - return result->value.boolean_value ? 1.0 : 0.0; - - case XPATH_RESULT_STRING: { - if (!result->value.string_value) return NAN; - - const char* str = result->value.string_value; - - /* Skip leading whitespace */ - while (isspace((unsigned char)*str)) str++; - - /* Empty string or only whitespace -> NaN */ - if (*str == '\0') return NAN; - - /* Try to parse as number */ - char* endptr; - double value = strtod(str, &endptr); - - /* Skip trailing whitespace */ - while (isspace((unsigned char)*endptr)) endptr++; - - /* If we didn't consume the entire string (after trimming), it's NaN */ - if (*endptr != '\0') return NAN; - - return value; - } - - case XPATH_RESULT_NODESET: { - /* Convert first node's string value to number */ - XPathNodeSet* nodeset = result->value.nodeset_value; - if (!nodeset || xpath_nodeset_count(nodeset) == 0) { - return NAN; - } - void* first_node = xpath_nodeset_get(nodeset, 0); - char* str = get_node_text(first_node); - - /* Parse the string */ - const char* p = str; - while (isspace((unsigned char)*p)) p++; - - if (*p == '\0') { - TAURUS_FREE(str); - return NAN; - } - - char* endptr; - double value = strtod(p, &endptr); - - while (isspace((unsigned char)*endptr)) endptr++; - - if (*endptr != '\0') { - TAURUS_FREE(str); - return NAN; - } - - TAURUS_FREE(str); - return value; - } - - default: - return NAN; - } -} - -/* ============================================================================ - * UTF-8 Helper Functions - * ============================================================================ */ - -/* Count UTF-8 characters (not bytes) in a string */ -static size_t utf8_strlen(const char* str) { - if (!str) return 0; - - size_t count = 0; - const unsigned char* p = (const unsigned char*)str; - - while (*p) { - /* Count leading bytes only (not continuation bytes 10xxxxxx) */ - if ((*p & 0xC0) != 0x80) { - count++; - } - p++; - } - - return count; -} - -/* Get byte offset for UTF-8 character at position (0-based character index) */ -static size_t utf8_char_offset(const char* str, size_t char_pos) { - if (!str) return 0; - - size_t count = 0; - size_t offset = 0; - const unsigned char* p = (const unsigned char*)str; - - /* Special case: position 0 is offset 0 */ - if (char_pos == 0) return 0; - - while (*p) { - /* Move to next byte */ - offset++; - p++; - - /* Check if we've reached a new character (not a continuation byte) */ - if (*p && (*p & 0xC0) != 0x80) { - count++; - if (count == char_pos) { - return offset; - } - } - } - - /* If we've gone past the end, return the string length */ - return offset; -} - -/* Extract substring by character positions (0-based character indices) */ -static char* utf8_substring(const char* str, size_t start_char, size_t char_count) { - if (!str || char_count == 0) return taurus_strdup(""); - - size_t str_len_chars = utf8_strlen(str); - - /* Clamp start position to valid range */ - if (start_char >= str_len_chars) { - return taurus_strdup(""); - } - - /* Clamp character count to available characters */ - if (start_char + char_count > str_len_chars) { - char_count = str_len_chars - start_char; - } - - /* Get byte offsets */ - size_t start_byte = utf8_char_offset(str, start_char); - size_t end_char = start_char + char_count; - size_t end_byte = utf8_char_offset(str, end_char); - - size_t length = end_byte - start_byte; - if (length == 0) return taurus_strdup(""); - - char* result = TAURUS_ALLOC_N(char, length + 1); - if (!result) return taurus_strdup(""); - - memcpy(result, str + start_byte, length); - result[length] = '\0'; - - return result; -} - -/* ============================================================================ - * Core XPath 1.0 Functions - * ============================================================================ */ - -/* last() - Returns the context size (number of nodes in context nodeset) */ -static struct taurus_xpath_result* xpath_func_last( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - (void)args; /* Unused */ - - if (arg_count != 0) { - snprintf(context->error_msg, sizeof(context->error_msg), - "last() takes no arguments"); - return NULL; - } - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER); - if (!result) return NULL; - - result->value.number_value = (double)context->context_size; - return result; -} - -/* position() - Returns the context position (1-based) */ -static struct taurus_xpath_result* xpath_func_position( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - (void)args; /* Unused */ - - if (arg_count != 0) { - snprintf(context->error_msg, sizeof(context->error_msg), - "position() takes no arguments"); - return NULL; - } - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER); - if (!result) return NULL; - - result->value.number_value = (double)context->context_position; - return result; -} - -/* ============================================================================ - * XPath String Functions - * ============================================================================ */ - -/* string(object?) - Convert argument to string */ -static struct taurus_xpath_result* xpath_func_string( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (!result) return NULL; - - if (arg_count == 0) { - /* No argument: convert context node to string */ - result->value.string_value = get_element_text(context->context_node); - } else { - /* Evaluate argument and convert to string */ - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) { - xpath_result_free(result); - return NULL; - } - - result->value.string_value = result_to_string(arg_result); - xpath_result_free(arg_result); - } - - return result; -} - -/* concat(string, string, string*) - Concatenate strings */ -static struct taurus_xpath_result* xpath_func_concat( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count < 2) { - snprintf(context->error_msg, sizeof(context->error_msg), - "concat() requires at least 2 arguments"); - return NULL; - } - - /* Calculate total length needed */ - size_t total_length = 0; - char** strings = TAURUS_ALLOC_N(char*, arg_count); - if (!strings) return NULL; - - for (size_t i = 0; i < arg_count; i++) { - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[i]); - if (!arg_result) { - /* Cleanup and return NULL */ - for (size_t j = 0; j < i; j++) { - TAURUS_FREE(strings[j]); - } - TAURUS_FREE(strings); - return NULL; - } - - strings[i] = result_to_string(arg_result); - total_length += strlen(strings[i]); - xpath_result_free(arg_result); - } - - /* Allocate result string */ - char* concat_str = TAURUS_ALLOC_N(char, total_length + 1); - if (!concat_str) { - for (size_t i = 0; i < arg_count; i++) { - TAURUS_FREE(strings[i]); - } - TAURUS_FREE(strings); - return NULL; - } - - /* Concatenate all strings */ - concat_str[0] = '\0'; - for (size_t i = 0; i < arg_count; i++) { - strcat(concat_str, strings[i]); - TAURUS_FREE(strings[i]); - } - TAURUS_FREE(strings); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (!result) { - TAURUS_FREE(concat_str); - return NULL; - } - result->value.string_value = concat_str; - - return result; -} - -/* starts-with(string, string) - Check if string starts with prefix */ -static struct taurus_xpath_result* xpath_func_starts_with( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 2) { - snprintf(context->error_msg, sizeof(context->error_msg), - "starts-with() requires exactly 2 arguments"); - return NULL; - } - - struct taurus_xpath_result* str_result = xpath_evaluate(context, args[0]); - if (!str_result) return NULL; - - struct taurus_xpath_result* prefix_result = xpath_evaluate(context, args[1]); - if (!prefix_result) { - xpath_result_free(str_result); - return NULL; - } - - char* str = result_to_string(str_result); - char* prefix = result_to_string(prefix_result); - - int match = (strncmp(str, prefix, strlen(prefix)) == 0); - - TAURUS_FREE(str); - TAURUS_FREE(prefix); - xpath_result_free(str_result); - xpath_result_free(prefix_result); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_BOOLEAN); - if (!result) return NULL; - result->value.boolean_value = match; - - return result; -} - -/* contains(string, string) - Check if string contains substring */ -static struct taurus_xpath_result* xpath_func_contains( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 2) { - snprintf(context->error_msg, sizeof(context->error_msg), - "contains() requires exactly 2 arguments"); - return NULL; - } - - struct taurus_xpath_result* str_result = xpath_evaluate(context, args[0]); - if (!str_result) return NULL; - - struct taurus_xpath_result* substr_result = xpath_evaluate(context, args[1]); - if (!substr_result) { - xpath_result_free(str_result); - return NULL; - } - - char* str = result_to_string(str_result); - char* substr = result_to_string(substr_result); - - int match = (strstr(str, substr) != NULL); - - TAURUS_FREE(str); - TAURUS_FREE(substr); - xpath_result_free(str_result); - xpath_result_free(substr_result); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_BOOLEAN); - if (!result) return NULL; - result->value.boolean_value = match; - - return result; -} - -/* substring(string, number, number?) - Extract substring (1-BASED INDEXING!) */ -static struct taurus_xpath_result* xpath_func_substring( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count < 2 || arg_count > 3) { - snprintf(context->error_msg, sizeof(context->error_msg), - "substring() requires 2 or 3 arguments"); - return NULL; - } - - /* Get string argument */ - struct taurus_xpath_result* str_result = xpath_evaluate(context, args[0]); - if (!str_result) return NULL; - char* str = result_to_string(str_result); - xpath_result_free(str_result); - - /* Get start position (1-based!) */ - struct taurus_xpath_result* start_result = xpath_evaluate(context, args[1]); - if (!start_result) { - TAURUS_FREE(str); - return NULL; - } - - double start_pos = result_to_number(start_result); - xpath_result_free(start_result); - - /* Get optional length */ - double length = INFINITY; - if (arg_count == 3) { - struct taurus_xpath_result* len_result = xpath_evaluate(context, args[2]); - if (!len_result) { - TAURUS_FREE(str); - return NULL; - } - - length = result_to_number(len_result); - xpath_result_free(len_result); - } - - /* Handle NaN and special values per XPath spec */ - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (!result) { - TAURUS_FREE(str); - return NULL; - } - - if (isnan(start_pos) || isnan(length)) { - result->value.string_value = taurus_strdup(""); - TAURUS_FREE(str); - return result; - } - - /* Round to nearest according to XPath spec */ - start_pos = round(start_pos); - length = round(length); - - /* v1.1.0: Handle negative or zero start positions per XPath 1.0 spec - * XPath uses 1-based indexing, so position < 1 is "before the string" - * Example: substring("12345", 0, 3) covers positions [0, 1, 2] - * Only position 1 is valid, so result is "1" */ - if (start_pos < 1.0) { - /* Adjust length to account for positions before position 1 */ - double chars_before = 1.0 - start_pos; - length -= chars_before; - start_pos = 1.0; - - /* If adjusted length is now non-positive, return empty */ - if (length <= 0.0) { - result->value.string_value = taurus_strdup(""); - TAURUS_FREE(str); - return result; - } - } - - /* XPath substring extracts characters at positions [start_pos, start_pos + length) - * in 1-based indexing. We need to find the intersection with valid range [1, str_len] */ - - size_t str_len = utf8_strlen(str); - - if (length <= 0) { - result->value.string_value = taurus_strdup(""); - TAURUS_FREE(str); - return result; - } - - /* Calculate the range of 1-based positions we want: [start_pos, start_pos + length) */ - double end_pos = start_pos + length; - - /* Intersect with valid range [1, str_len + 1) - * (end is exclusive, so str_len + 1 is one past the last character) */ - double actual_start_1based = start_pos < 1.0 ? 1.0 : start_pos; - double actual_end_1based = end_pos > (double)(str_len + 1) ? - (double)(str_len + 1) : end_pos; - - /* If no intersection, return empty */ - if (actual_start_1based >= actual_end_1based || - actual_start_1based > (double)str_len) { - result->value.string_value = taurus_strdup(""); - TAURUS_FREE(str); - return result; - } - - /* Calculate how many characters to extract */ - size_t actual_length = (size_t)(actual_end_1based - actual_start_1based); - - /* Convert to 0-based for C string indexing */ - size_t actual_start = (size_t)(actual_start_1based - 1); - - /* Extract substring using UTF-8 aware function */ - result->value.string_value = utf8_substring(str, actual_start, actual_length); - TAURUS_FREE(str); - - return result; -} - -/* string-length(string?) - Get character length of string */ -static struct taurus_xpath_result* xpath_func_string_length( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - char* str; - - if (arg_count == 0) { - /* No argument: use context node string value */ - str = get_element_text(context->context_node); - } else { - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - str = result_to_string(arg_result); - xpath_result_free(arg_result); - } - - size_t length = utf8_strlen(str); - TAURUS_FREE(str); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER); - if (!result) return NULL; - result->value.number_value = (double)length; - - return result; -} - -/* normalize-space(string?) - Normalize whitespace */ -static struct taurus_xpath_result* xpath_func_normalize_space( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - char* str; - - if (arg_count == 0) { - /* No argument: use context node string value */ - str = get_element_text(context->context_node); - } else { - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - str = result_to_string(arg_result); - xpath_result_free(arg_result); - } - - /* Allocate buffer for normalized string (can't be longer than original) */ - char* normalized = TAURUS_ALLOC_N(char, strlen(str) + 1); - if (!normalized) { - TAURUS_FREE(str); - return NULL; - } - - char* out = normalized; - const char* in = str; - int in_space = 0; - int started = 0; - - /* Skip leading whitespace and collapse internal whitespace */ - while (*in) { - if (isspace((unsigned char)*in)) { - in_space = 1; - in++; - } else { - if (started && in_space) { - *out++ = ' '; - } - *out++ = *in++; - in_space = 0; - started = 1; - } - } - *out = '\0'; - - TAURUS_FREE(str); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (!result) { - TAURUS_FREE(normalized); - return NULL; - } - result->value.string_value = normalized; - - return result; -} - -/* ============================================================================ - * XPath Boolean Functions - * ============================================================================ */ - -/* boolean(object) - Convert any object to boolean */ -static struct taurus_xpath_result* xpath_func_boolean( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "boolean() requires exactly 1 argument"); - return NULL; - } - - /* Evaluate argument */ - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - /* Convert to boolean */ - int bool_value = result_to_boolean(arg_result); - xpath_result_free(arg_result); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_BOOLEAN); - if (!result) return NULL; - result->value.boolean_value = bool_value; - - return result; -} - -/* not(boolean) - Logical negation */ -static struct taurus_xpath_result* xpath_func_not( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "not() requires exactly 1 argument"); - return NULL; - } - - /* Evaluate argument */ - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - /* Convert to boolean and negate */ - int bool_value = result_to_boolean(arg_result); - xpath_result_free(arg_result); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_BOOLEAN); - if (!result) return NULL; - result->value.boolean_value = !bool_value; - - return result; -} - -/* true() - Returns boolean true */ -static struct taurus_xpath_result* xpath_func_true( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - (void)context; /* Unused */ - (void)args; /* Unused */ - - if (arg_count != 0) { - snprintf(context->error_msg, sizeof(context->error_msg), - "true() takes no arguments"); - return NULL; - } - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_BOOLEAN); - if (!result) return NULL; - result->value.boolean_value = 1; - - return result; -} - -/* false() - Returns boolean false */ -static struct taurus_xpath_result* xpath_func_false( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - (void)context; /* Unused */ - (void)args; /* Unused */ - - if (arg_count != 0) { - snprintf(context->error_msg, sizeof(context->error_msg), - "false() takes no arguments"); - return NULL; - } - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_BOOLEAN); - if (!result) return NULL; - result->value.boolean_value = 0; - - return result; -} - -/* ============================================================================ - * XPath Number Functions - * ============================================================================ */ - -/* number(object?) - Convert argument to number */ -static struct taurus_xpath_result* xpath_func_number( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count > 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "number() takes 0 or 1 argument"); - return NULL; - } - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER); - if (!result) return NULL; - - if (arg_count == 0) { - /* No argument: convert context node to number */ - char* str = get_element_text(context->context_node); - const char* p = str; - while (isspace((unsigned char)*p)) p++; - - if (*p == '\0') { - result->value.number_value = NAN; - } else { - char* endptr; - double value = strtod(p, &endptr); - while (isspace((unsigned char)*endptr)) endptr++; - result->value.number_value = (*endptr == '\0') ? value : NAN; - } - TAURUS_FREE(str); - } else { - /* Evaluate argument and convert to number */ - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) { - xpath_result_free(result); - return NULL; - } - result->value.number_value = result_to_number(arg_result); - xpath_result_free(arg_result); - } - return result; -} - -/* sum(node-set) - Sum the numeric values of all nodes */ -static struct taurus_xpath_result* xpath_func_sum( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "sum() requires exactly 1 argument"); - return NULL; - } - - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - if (arg_result->type != XPATH_RESULT_NODESET) { - snprintf(context->error_msg, sizeof(context->error_msg), - "sum() argument must be a nodeset"); - xpath_result_free(arg_result); - return NULL; - } - - XPathNodeSet* nodeset = arg_result->value.nodeset_value; - double sum = 0.0; - - if (nodeset) { - size_t count = xpath_nodeset_count(nodeset); - for (size_t i = 0; i < count; i++) { - void* node = xpath_nodeset_get(nodeset, i); - char* str = get_node_text(node); - const char* p = str; - while (isspace((unsigned char)*p)) p++; - - if (*p != '\0') { - char* endptr; - double value = strtod(p, &endptr); - while (isspace((unsigned char)*endptr)) endptr++; - if (*endptr == '\0' && !isnan(value)) { - sum += value; - } - } - TAURUS_FREE(str); - } - } - xpath_result_free(arg_result); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER); - if (!result) return NULL; - result->value.number_value = sum; - return result; -} - -/* floor(number) - Largest integer not greater than argument */ -static struct taurus_xpath_result* xpath_func_floor( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "floor() requires exactly 1 argument"); - return NULL; - } - - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - double num = result_to_number(arg_result); - xpath_result_free(arg_result); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER); - if (!result) return NULL; - result->value.number_value = floor(num); - return result; -} - -/* ceiling(number) - Smallest integer not less than argument */ -static struct taurus_xpath_result* xpath_func_ceiling( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "ceiling() requires exactly 1 argument"); - return NULL; - } - - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - double num = result_to_number(arg_result); - xpath_result_free(arg_result); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER); - if (!result) return NULL; - result->value.number_value = ceil(num); - return result; -} - -/* round(number) - Round to nearest integer (half away from zero) */ -static struct taurus_xpath_result* xpath_func_round( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "round() requires exactly 1 argument"); - return NULL; - } - - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - double num = result_to_number(arg_result); - xpath_result_free(arg_result); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER); - if (!result) return NULL; - - /* XPath round() rounds half away from zero */ - if (isnan(num) || isinf(num) || num == 0.0) { - result->value.number_value = num; - } else { - double frac = num - floor(num); - if (frac == 0.5) { - result->value.number_value = (num > 0) ? ceil(num) : floor(num); - } else { - result->value.number_value = floor(num + 0.5); - } - } - return result; -} - -/* ============================================================================ - * XPath Node-set Functions - * ============================================================================ */ - -/* Helper to recursively find elements by id */ -static void find_elements_by_id(struct taurus_element* node, const char* id, - XPathNodeSet* result) { - if (!node) return; - - /* Check this node's id attribute */ - for (size_t i = 0; i < node->attributes_count; i++) { - struct taurus_attribute* attr = node->attributes[i]; - if (attr && attr->name && strcmp(attr->name, "id") == 0) { - if (attr->value && strcmp(attr->value, id) == 0) { - xpath_nodeset_add(result, node); - } - } - } - - /* Search children */ - for (size_t i = 0; i < node->children_count; i++) { - find_elements_by_id(node->children[i], id, result); - } -} - -/* count(node-set) - Returns the number of nodes */ -static struct taurus_xpath_result* xpath_func_count( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "count() requires exactly 1 argument"); - return NULL; - } - - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - if (arg_result->type != XPATH_RESULT_NODESET) { - snprintf(context->error_msg, sizeof(context->error_msg), - "count() argument must be a nodeset"); - xpath_result_free(arg_result); - return NULL; - } - - XPathNodeSet* nodeset = arg_result->value.nodeset_value; - size_t count = nodeset ? xpath_nodeset_count(nodeset) : 0; - xpath_result_free(arg_result); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER); - if (!result) return NULL; - result->value.number_value = (double)count; - return result; -} - -/* local-name(node-set?) - Local name of first node */ -static struct taurus_xpath_result* xpath_func_local_name( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count > 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "local-name() takes 0 or 1 argument"); - return NULL; - } - - struct taurus_element* node; - if (arg_count == 0) { - node = context->context_node; - } else { - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - if (arg_result->type != XPATH_RESULT_NODESET) { - xpath_result_free(arg_result); - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (result) result->value.string_value = taurus_strdup(""); - return result; - } - - XPathNodeSet* nodeset = arg_result->value.nodeset_value; - if (!nodeset || xpath_nodeset_count(nodeset) == 0) { - xpath_result_free(arg_result); - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (result) result->value.string_value = taurus_strdup(""); - return result; - } - - node = xpath_nodeset_get(nodeset, 0); - xpath_result_free(arg_result); - } - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (!result) return NULL; - - if (!node || !node->name) { - result->value.string_value = taurus_strdup(""); - return result; - } - - /* Return local name (after ':' if present) */ - const char* colon = strchr(node->name, ':'); - result->value.string_value = taurus_strdup(colon ? colon + 1 : node->name); - return result; -} - -/* namespace-uri(node-set?) - Namespace URI of first node */ -static struct taurus_xpath_result* xpath_func_namespace_uri( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count > 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "namespace-uri() takes 0 or 1 argument"); - return NULL; - } - - struct taurus_element* node; - if (arg_count == 0) { - node = context->context_node; - } else { - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - if (arg_result->type != XPATH_RESULT_NODESET) { - xpath_result_free(arg_result); - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (result) result->value.string_value = taurus_strdup(""); - return result; - } - - XPathNodeSet* nodeset = arg_result->value.nodeset_value; - if (!nodeset || xpath_nodeset_count(nodeset) == 0) { - xpath_result_free(arg_result); - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (result) result->value.string_value = taurus_strdup(""); - return result; - } - - node = xpath_nodeset_get(nodeset, 0); - xpath_result_free(arg_result); - } - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (!result) return NULL; - - result->value.string_value = (node && node->namespace_uri) ? - taurus_strdup(node->namespace_uri) : - taurus_strdup(""); - return result; -} - -/* name(node-set?) - Qualified name of first node */ -static struct taurus_xpath_result* xpath_func_name( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count > 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "name() takes 0 or 1 argument"); - return NULL; - } - - struct taurus_element* node; - if (arg_count == 0) { - node = context->context_node; - } else { - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - if (arg_result->type != XPATH_RESULT_NODESET) { - xpath_result_free(arg_result); - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (result) result->value.string_value = taurus_strdup(""); - return result; - } - - XPathNodeSet* nodeset = arg_result->value.nodeset_value; - if (!nodeset || xpath_nodeset_count(nodeset) == 0) { - xpath_result_free(arg_result); - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (result) result->value.string_value = taurus_strdup(""); - return result; - } - - node = xpath_nodeset_get(nodeset, 0); - xpath_result_free(arg_result); - } - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (!result) return NULL; - - /* XPath name() returns qualified name, but our implementation stores - * full qualified name in node->name (e.g., "ns:item"). - * Since we're matching local names everywhere else, return local name here too. - * TODO: When we properly store prefix separately, return prefix:localname */ - if (!node || !node->name) { - result->value.string_value = taurus_strdup(""); - return result; - } - - /* Strip namespace prefix if present */ - const char* colon = strchr(node->name, ':'); - result->value.string_value = taurus_strdup(colon ? colon + 1 : node->name); - return result; -} - -/* id(object) - Select elements by ID */ -static struct taurus_xpath_result* xpath_func_id( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "id() requires exactly 1 argument"); - return NULL; - } - - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - char** id_strings = NULL; - size_t id_count = 0; - - if (arg_result->type == XPATH_RESULT_NODESET) { - XPathNodeSet* nodeset = arg_result->value.nodeset_value; - if (nodeset) { - id_count = xpath_nodeset_count(nodeset); - if (id_count > 0) { - id_strings = TAURUS_ALLOC_N(char*, id_count); - if (id_strings) { - for (size_t i = 0; i < id_count; i++) { - id_strings[i] = get_node_text(xpath_nodeset_get(nodeset, i)); - } - } - } - } - } else { - char* str = result_to_string(arg_result); - const char* p = str; - int in_id = 0; - while (*p) { - if (isspace((unsigned char)*p)) { - in_id = 0; - } else if (!in_id) { - id_count++; - in_id = 1; - } - p++; - } - - if (id_count > 0) { - id_strings = TAURUS_ALLOC_N(char*, id_count); - if (id_strings) { - size_t idx = 0; - p = str; - while (*p && isspace((unsigned char)*p)) p++; - while (*p && idx < id_count) { - const char* start = p; - while (*p && !isspace((unsigned char)*p)) p++; - size_t len = p - start; - id_strings[idx] = TAURUS_ALLOC_N(char, len + 1); - if (id_strings[idx]) { - memcpy(id_strings[idx], start, len); - id_strings[idx][len] = '\0'; - idx++; - } - while (*p && isspace((unsigned char)*p)) p++; - } - } - } - TAURUS_FREE(str); - } - xpath_result_free(arg_result); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_NODESET); - if (!result) { - for (size_t i = 0; i < id_count; i++) TAURUS_FREE(id_strings[i]); - TAURUS_FREE(id_strings); - return NULL; - } - - result->value.nodeset_value = xpath_nodeset_new(); - if (!result->value.nodeset_value) { - xpath_result_free(result); - for (size_t i = 0; i < id_count; i++) TAURUS_FREE(id_strings[i]); - TAURUS_FREE(id_strings); - return NULL; - } - - if (id_count > 0 && context->document && context->document->root) { - for (size_t i = 0; i < id_count; i++) { - find_elements_by_id(context->document->root, id_strings[i], - result->value.nodeset_value); - } - } - - for (size_t i = 0; i < id_count; i++) TAURUS_FREE(id_strings[i]); - TAURUS_FREE(id_strings); - return result; -} - -/* translate(string, string, string) - Character translation */ -static struct taurus_xpath_result* xpath_func_translate( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 3) { - snprintf(context->error_msg, sizeof(context->error_msg), - "translate() requires 3 arguments"); - return NULL; - } - - struct taurus_xpath_result* results[3]; - for (int i = 0; i < 3; i++) { - results[i] = xpath_evaluate(context, args[i]); - if (!results[i]) { - for (int j = 0; j < i; j++) xpath_result_free(results[j]); - return NULL; - } - } - - char* str = result_to_string(results[0]); - char* from = result_to_string(results[1]); - char* to = result_to_string(results[2]); - - for (int i = 0; i < 3; i++) xpath_result_free(results[i]); - - size_t from_len = strlen(from); - size_t to_len = strlen(to); - char* translated = TAURUS_ALLOC_N(char, strlen(str) + 1); - if (!translated) { - TAURUS_FREE(str); - TAURUS_FREE(from); - TAURUS_FREE(to); - return NULL; - } - - char* out = translated; - const char* in = str; - while (*in) { - int found = -1; - for (size_t i = 0; i < from_len; i++) { - if (*in == from[i]) { - found = (int)i; - break; - } - } - if (found >= 0 && (size_t)found < to_len) { - *out++ = to[found]; - } else if (found < 0) { - *out++ = *in; - } - in++; - } - *out = '\0'; - - TAURUS_FREE(str); - TAURUS_FREE(from); - TAURUS_FREE(to); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (!result) { - TAURUS_FREE(translated); - return NULL; - } - result->value.string_value = translated; - return result; -} - -/* substring-before(string, string) - Before delimiter */ -static struct taurus_xpath_result* xpath_func_substring_before( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 2) { - snprintf(context->error_msg, sizeof(context->error_msg), - "substring-before() requires 2 arguments"); - return NULL; - } - - struct taurus_xpath_result* results[2]; - for (int i = 0; i < 2; i++) { - results[i] = xpath_evaluate(context, args[i]); - if (!results[i]) { - for (int j = 0; j < i; j++) xpath_result_free(results[j]); - return NULL; - } - } - - char* str = result_to_string(results[0]); - char* delim = result_to_string(results[1]); - xpath_result_free(results[0]); - xpath_result_free(results[1]); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (!result) { - TAURUS_FREE(str); - TAURUS_FREE(delim); - return NULL; - } - - if (delim[0] == '\0') { - result->value.string_value = taurus_strdup(""); - } else { - const char* pos = strstr(str, delim); - if (pos) { - size_t len = pos - str; - char* substr = TAURUS_ALLOC_N(char, len + 1); - if (substr) { - memcpy(substr, str, len); - substr[len] = '\0'; - result->value.string_value = substr; - } else { - result->value.string_value = taurus_strdup(""); - } - } else { - result->value.string_value = taurus_strdup(""); - } - } - - TAURUS_FREE(str); - TAURUS_FREE(delim); - return result; -} - -/* substring-after(string, string) - After delimiter */ -static struct taurus_xpath_result* xpath_func_substring_after( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 2) { - snprintf(context->error_msg, sizeof(context->error_msg), - "substring-after() requires 2 arguments"); - return NULL; - } - - struct taurus_xpath_result* results[2]; - for (int i = 0; i < 2; i++) { - results[i] = xpath_evaluate(context, args[i]); - if (!results[i]) { - for (int j = 0; j < i; j++) xpath_result_free(results[j]); - return NULL; - } - } - - char* str = result_to_string(results[0]); - char* delim = result_to_string(results[1]); - xpath_result_free(results[0]); - xpath_result_free(results[1]); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING); - if (!result) { - TAURUS_FREE(str); - TAURUS_FREE(delim); - return NULL; - } - - if (delim[0] == '\0') { - result->value.string_value = taurus_strdup(str); - } else { - const char* pos = strstr(str, delim); - result->value.string_value = pos ? taurus_strdup(pos + strlen(delim)) : taurus_strdup(""); - } - - TAURUS_FREE(str); - TAURUS_FREE(delim); - return result; -} - -/* Helper to get xml:lang attribute */ -static char* get_lang_attribute(struct taurus_element* node) { - if (!node) return NULL; - - for (size_t i = 0; i < node->attributes_count; i++) { - struct taurus_attribute* attr = node->attributes[i]; - if (attr && attr->name && strcmp(attr->name, "xml:lang") == 0) { - return attr->value ? taurus_strdup(attr->value) : NULL; - } - } - - return node->parent ? get_lang_attribute(node->parent) : NULL; -} - -/* lang(string) - Check xml:lang matches */ -static struct taurus_xpath_result* xpath_func_lang( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -) { - if (arg_count != 1) { - snprintf(context->error_msg, sizeof(context->error_msg), - "lang() requires 1 argument"); - return NULL; - } - - struct taurus_xpath_result* arg_result = xpath_evaluate(context, args[0]); - if (!arg_result) return NULL; - - char* target_lang = result_to_string(arg_result); - xpath_result_free(arg_result); - - struct taurus_xpath_result* result = xpath_result_new(XPATH_RESULT_BOOLEAN); - if (!result) return NULL; - - char* node_lang = get_lang_attribute(context->context_node); - if (!node_lang) { - result->value.boolean_value = 0; - } else { - size_t target_len = strlen(target_lang); - size_t node_len = strlen(node_lang); - int matches = (node_len >= target_len && - strncasecmp(node_lang, target_lang, target_len) == 0 && - (node_len == target_len || node_lang[target_len] == '-')); - result->value.boolean_value = matches; - TAURUS_FREE(node_lang); - } - - TAURUS_FREE(target_lang); - return result; -} - -/* ============================================================================ - * Standard Function Library Initialization - * ============================================================================ */ - -void xpath_function_registry_init_standard(XPathFunctionRegistry* registry) { - if (!registry) return; - - /* Core functions */ - xpath_function_registry_register(registry, "last", xpath_func_last, 0, 0); - xpath_function_registry_register(registry, "position", xpath_func_position, 0, 0); - - /* String functions */ - xpath_function_registry_register(registry, "string", xpath_func_string, 0, 1); - xpath_function_registry_register(registry, "concat", xpath_func_concat, 2, -1); - xpath_function_registry_register(registry, "starts-with", xpath_func_starts_with, 2, 2); - xpath_function_registry_register(registry, "contains", xpath_func_contains, 2, 2); - xpath_function_registry_register(registry, "substring", xpath_func_substring, 2, 3); - xpath_function_registry_register(registry, "string-length", xpath_func_string_length, 0, 1); - xpath_function_registry_register(registry, "normalize-space", xpath_func_normalize_space, 0, 1); - xpath_function_registry_register(registry, "translate", xpath_func_translate, 3, 3); - xpath_function_registry_register(registry, "substring-before", xpath_func_substring_before, 2, 2); - xpath_function_registry_register(registry, "substring-after", xpath_func_substring_after, 2, 2); - - /* Boolean functions */ - xpath_function_registry_register(registry, "boolean", xpath_func_boolean, 1, 1); - xpath_function_registry_register(registry, "not", xpath_func_not, 1, 1); - xpath_function_registry_register(registry, "true", xpath_func_true, 0, 0); - xpath_function_registry_register(registry, "false", xpath_func_false, 0, 0); - xpath_function_registry_register(registry, "lang", xpath_func_lang, 1, 1); - - /* Number functions */ - xpath_function_registry_register(registry, "number", xpath_func_number, 0, 1); - xpath_function_registry_register(registry, "sum", xpath_func_sum, 1, 1); - xpath_function_registry_register(registry, "floor", xpath_func_floor, 1, 1); - xpath_function_registry_register(registry, "ceiling", xpath_func_ceiling, 1, 1); - xpath_function_registry_register(registry, "round", xpath_func_round, 1, 1); - - /* Node-set functions */ - xpath_function_registry_register(registry, "count", xpath_func_count, 1, 1); - xpath_function_registry_register(registry, "id", xpath_func_id, 1, 1); - xpath_function_registry_register(registry, "local-name", xpath_func_local_name, 0, 1); - xpath_function_registry_register(registry, "namespace-uri", xpath_func_namespace_uri, 0, 1); - xpath_function_registry_register(registry, "name", xpath_func_name, 0, 1); -} - -/* ============================================================================ - * Public XPath Function Support API (for libtaurus) - * ============================================================================ */ - -/* Array of all supported function names (NULL-terminated) */ -static const char* g_supported_functions[] = { - /* Core */ - "last", - "position", - /* String */ - "string", - "concat", - "starts-with", - "contains", - "substring-before", - "substring-after", - "substring", - "string-length", - "normalize-space", - "translate", - /* Boolean */ - "boolean", - "not", - "true", - "false", - "lang", - /* Number */ - "number", - "sum", - "floor", - "ceiling", - "round", - /* Node-set */ - "count", - "id", - "local-name", - "namespace-uri", - "name", - NULL /* Terminator */ -}; - -/** - * Check if XPath function is supported - */ -TAURUS_API int taurus_xpath_function_supported(const char* function_name) { - if (!function_name) return 0; - - for (size_t i = 0; g_supported_functions[i] != NULL; i++) { - if (strcmp(function_name, g_supported_functions[i]) == 0) { - return 1; - } - } - return 0; -} - -/** - * Get list of supported XPath functions - */ -TAURUS_API const char** taurus_xpath_supported_functions(void) { - return g_supported_functions; -} diff --git a/ext/taurus/lib/src/xpath/functions.h b/ext/taurus/lib/src/xpath/functions.h deleted file mode 100644 index 278a688..0000000 --- a/ext/taurus/lib/src/xpath/functions.h +++ /dev/null @@ -1,137 +0,0 @@ -/* functions.h - XPath 1.0 function library API - * Copyright (c) 2024, Ribose Inc. - * - * Pure C implementation of XPath 1.0 standard function library. - * Provides all 27 XPath 1.0 functions with extensible registry. - */ - -#ifndef XPATH_FUNCTIONS_H -#define XPATH_FUNCTIONS_H - -#include "../taurus_internal.h" -#include "parser.h" - -/* ============================================================================ - * Forward Declarations - * ============================================================================ */ - -typedef struct xpath_function_registry XPathFunctionRegistry; - -/* ============================================================================ - * Function Handler Type - * ============================================================================ */ - -/** - * XPath function handler - * - * @param context Evaluation context - * @param args Array of pointers to AST nodes (function arguments) - * @param arg_count Number of arguments - * @return Result of function evaluation, or NULL on error - */ -typedef struct taurus_xpath_result* (*XPathFunctionHandler)( - XPathContext* context, - XPathASTNode** args, - size_t arg_count -); - -/* ============================================================================ - * Function Definition Structure - * ============================================================================ */ - -/** - * Function definition - */ -typedef struct { - const char* name; /* Function name */ - XPathFunctionHandler handler; /* Handler function */ - int min_args; /* Minimum arguments */ - int max_args; /* Maximum arguments (-1 = unlimited) */ -} XPathFunctionDef; - -/* ============================================================================ - * Function Registry Structure - * ============================================================================ */ - -/** - * Function registry - * Stores registered XPath functions - */ -struct xpath_function_registry { - XPathFunctionDef* functions; - size_t count; - size_t capacity; -}; - -/* ============================================================================ - * Registry Management - * ============================================================================ */ - -/** - * Create new function registry - * - * @return New registry, or NULL on error - */ -XPathFunctionRegistry* xpath_function_registry_new(void); - -/** - * Free function registry - * - * @param registry Registry to free - */ -void xpath_function_registry_free(XPathFunctionRegistry* registry); - -/** - * Register a function - * - * @param registry Registry to register in - * @param name Function name - * @param handler Handler function - * @param min_args Minimum arguments - * @param max_args Maximum arguments (-1 for unlimited) - */ -void xpath_function_registry_register( - XPathFunctionRegistry* registry, - const char* name, - XPathFunctionHandler handler, - int min_args, - int max_args -); - -/** - * Lookup function by name - * - * @param registry Registry to search - * @param name Function name - * @return Handler function, or NULL if not found - */ -XPathFunctionHandler xpath_function_registry_lookup( - XPathFunctionRegistry* registry, - const char* name -); - -/** - * Get function definition by name - * - * @param registry Registry to search - * @param name Function name - * @return Function definition, or NULL if not found - */ -XPathFunctionDef* xpath_function_registry_get( - XPathFunctionRegistry* registry, - const char* name -); - -/* ============================================================================ - * Standard Function Library - * ============================================================================ */ - -/** - * Initialize standard XPath 1.0 functions - * Registers all 27 XPath 1.0 functions - * - * @param registry Registry to initialize - */ -void xpath_function_registry_init_standard(XPathFunctionRegistry* registry); - -#endif /* XPATH_FUNCTIONS_H */ \ No newline at end of file diff --git a/ext/taurus/lib/src/xpath/lexer.c b/ext/taurus/lib/src/xpath/lexer.c deleted file mode 100644 index b1687ef..0000000 --- a/ext/taurus/lib/src/xpath/lexer.c +++ /dev/null @@ -1,554 +0,0 @@ -/* lexer.c - XPath 1.0 Lexer (Pure C) - * Copyright (c) 2024, Ribose Inc. - */ - -#include "lexer.h" -#include "../taurus_internal.h" -#include -#include -#include - -/* Token type names for debugging */ -const char* xpath_token_type_names[] = { - "EOF", - "SLASH", - "DOUBLE_SLASH", - "AT", - "DOT", - "DOUBLE_DOT", - "LPAREN", - "RPAREN", - "LBRACKET", - "RBRACKET", - "COMMA", - "DOUBLE_COLON", - "NCNAME", - "QNAME", - "STRING", - "NUMBER", - "EQUALS", - "NOT_EQUALS", - "LT", - "LE", - "GT", - "GE", - "PLUS", - "MINUS", - "STAR", - "PIPE", - "AND", - "OR", - "DIV", - "MOD", - "ANCESTOR", - "ANCESTOR_OR_SELF", - "ATTRIBUTE", - "CHILD", - "DESCENDANT", - "DESCENDANT_OR_SELF", - "FOLLOWING", - "FOLLOWING_SIBLING", - "NAMESPACE", - "PARENT", - "PRECEDING", - "PRECEDING_SIBLING", - "SELF", - "COMMENT", - "TEXT", - "PROCESSING_INSTRUCTION", - "NODE" -}; - -/* Forward declarations */ -static void skip_whitespace(XPathLexer* lexer); -static int is_ncname_start(char c); -static int is_ncname_char(char c); -static XPathTokenType check_keyword(const char* str, size_t len, int peek_ahead, XPathLexer* lexer); - -/* Create a new lexer */ -XPathLexer* xpath_lexer_new(const char* input, size_t len) { - if (!input) return NULL; - - XPathLexer* lexer = TAURUS_ALLOC(XPathLexer); - if (!lexer) return NULL; - - lexer->input = input; - lexer->pos = input; - lexer->end = input + len; - lexer->line = 1; - lexer->column = 1; - lexer->error_msg[0] = '\0'; - - return lexer; -} - -/* Free the lexer */ -void xpath_lexer_free(XPathLexer* lexer) { - if (lexer) { - TAURUS_FREE(lexer); - } -} - -/* Check if character is NCName start character */ -static int is_ncname_start(char c) { - return isalpha((unsigned char)c) || c == '_'; -} - -/* Check if character is NCName character */ -static int is_ncname_char(char c) { - return isalnum((unsigned char)c) || c == '_' || c == '-' || c == '.'; -} - -/* Skip whitespace and update position */ -static void skip_whitespace(XPathLexer* lexer) { - while (lexer->pos < lexer->end && isspace((unsigned char)*lexer->pos)) { - if (*lexer->pos == '\n') { - lexer->line++; - lexer->column = 1; - } else { - lexer->column++; - } - lexer->pos++; - } -} - -/* Check if a string matches a keyword and return appropriate token type */ -static XPathTokenType check_keyword(const char* str, size_t len, int peek_ahead, XPathLexer* lexer) { - /* Check for axis names (must be followed by ::) */ - if (peek_ahead && lexer->pos < lexer->end - 1 && - lexer->pos[0] == ':' && lexer->pos[1] == ':') { - if (len == 8 && strncmp(str, "ancestor", 8) == 0) return TOK_ANCESTOR; - if (len == 16 && strncmp(str, "ancestor-or-self", 16) == 0) return TOK_ANCESTOR_OR_SELF; - if (len == 9 && strncmp(str, "attribute", 9) == 0) return TOK_ATTRIBUTE; - if (len == 5 && strncmp(str, "child", 5) == 0) return TOK_CHILD; - if (len == 10 && strncmp(str, "descendant", 10) == 0) return TOK_DESCENDANT; - if (len == 18 && strncmp(str, "descendant-or-self", 18) == 0) return TOK_DESCENDANT_OR_SELF; - if (len == 9 && strncmp(str, "following", 9) == 0) return TOK_FOLLOWING; - if (len == 17 && strncmp(str, "following-sibling", 17) == 0) return TOK_FOLLOWING_SIBLING; - if (len == 9 && strncmp(str, "namespace", 9) == 0) return TOK_NAMESPACE; - if (len == 6 && strncmp(str, "parent", 6) == 0) return TOK_PARENT; - if (len == 9 && strncmp(str, "preceding", 9) == 0) return TOK_PRECEDING; - if (len == 17 && strncmp(str, "preceding-sibling", 17) == 0) return TOK_PRECEDING_SIBLING; - if (len == 4 && strncmp(str, "self", 4) == 0) return TOK_SELF; - } - - /* Check for node type tests (must be followed by '(') */ - if (peek_ahead && lexer->pos < lexer->end && *lexer->pos == '(') { - if (len == 7 && strncmp(str, "comment", 7) == 0) return TOK_COMMENT; - if (len == 4 && strncmp(str, "text", 4) == 0) return TOK_TEXT; - if (len == 4 && strncmp(str, "node", 4) == 0) return TOK_NODE; - if (len == 22 && strncmp(str, "processing-instruction", 22) == 0) - return TOK_PROCESSING_INSTRUCTION; - } - - /* Check for operator keywords (can appear anywhere) */ - if (len == 3 && strncmp(str, "and", 3) == 0) return TOK_AND; - if (len == 2 && strncmp(str, "or", 2) == 0) return TOK_OR; - if (len == 3 && strncmp(str, "div", 3) == 0) return TOK_DIV; - if (len == 3 && strncmp(str, "mod", 3) == 0) return TOK_MOD; - - return TOK_NCNAME; -} - -/* Get next token */ -XPathToken xpath_lexer_next_token(XPathLexer* lexer) { - XPathToken token; - - if (!lexer) { - token.type = TOK_EOF; - token.value = NULL; - token.value_len = 0; - token.line = 0; - token.column = 0; - return token; - } - - skip_whitespace(lexer); - - token.line = lexer->line; - token.column = lexer->column; - - if (lexer->pos >= lexer->end) { - token.type = TOK_EOF; - token.value = lexer->pos; - token.value_len = 0; - return token; - } - - char c = *lexer->pos; - - /* Single character tokens */ - switch (c) { - case '@': - token.type = TOK_AT; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - - case '(': - token.type = TOK_LPAREN; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - - case ')': - token.type = TOK_RPAREN; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - - case '[': - token.type = TOK_LBRACKET; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - - case ']': - token.type = TOK_RBRACKET; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - - case ',': - token.type = TOK_COMMA; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - - case '+': - token.type = TOK_PLUS; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - - case '-': - token.type = TOK_MINUS; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - - case '*': - token.type = TOK_STAR; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - - case '|': - token.type = TOK_PIPE; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - } - - /* Multi-character tokens */ - - /* Slash and double slash */ - if (c == '/') { - if (lexer->pos + 1 < lexer->end && lexer->pos[1] == '/') { - token.type = TOK_DOUBLE_SLASH; - token.value = lexer->pos; - token.value_len = 2; - lexer->pos += 2; - lexer->column += 2; - return token; - } - token.type = TOK_SLASH; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - } - - /* Dot and double dot */ - if (c == '.') { - if (lexer->pos + 1 < lexer->end && lexer->pos[1] == '.') { - token.type = TOK_DOUBLE_DOT; - token.value = lexer->pos; - token.value_len = 2; - lexer->pos += 2; - lexer->column += 2; - return token; - } - /* Check if it's a number like .5 */ - if (lexer->pos + 1 < lexer->end && isdigit((unsigned char)lexer->pos[1])) { - goto parse_number; - } - token.type = TOK_DOT; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - } - - /* Colon (for :: and QNames) */ - if (c == ':') { - if (lexer->pos + 1 < lexer->end && lexer->pos[1] == ':') { - token.type = TOK_DOUBLE_COLON; - token.value = lexer->pos; - token.value_len = 2; - lexer->pos += 2; - lexer->column += 2; - return token; - } - /* Single colon is handled as part of QName */ - if (lexer->input && lexer->pos >= lexer->input) { - size_t byte_offset = lexer->pos - lexer->input; - - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_SYNTAX, - "Unexpected ':' character in XPath expression", - lexer->input, - byte_offset, - lexer->line, - lexer->column - ); - } - - snprintf(lexer->error_msg, sizeof(lexer->error_msg), - "Unexpected character ':' at line %d, column %d", - lexer->line, lexer->column); - token.type = TOK_EOF; /* Error token */ - token.value = lexer->pos; - token.value_len = 0; - return token; - } - - /* Comparison operators */ - if (c == '=') { - token.type = TOK_EQUALS; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - } - - if (c == '!') { - if (lexer->pos + 1 < lexer->end && lexer->pos[1] == '=') { - token.type = TOK_NOT_EQUALS; - token.value = lexer->pos; - token.value_len = 2; - lexer->pos += 2; - lexer->column += 2; - return token; - } - if (lexer->input && lexer->pos >= lexer->input) { - size_t byte_offset = lexer->pos - lexer->input; - - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_SYNTAX, - "Unexpected '!' character (did you mean '!='?)", - lexer->input, - byte_offset, - lexer->line, - lexer->column - ); - } - - snprintf(lexer->error_msg, sizeof(lexer->error_msg), - "Unexpected character '!' at line %d, column %d", - lexer->line, lexer->column); - token.type = TOK_EOF; /* Error token */ - token.value = lexer->pos; - token.value_len = 0; - return token; - } - - if (c == '<') { - if (lexer->pos + 1 < lexer->end && lexer->pos[1] == '=') { - token.type = TOK_LE; - token.value = lexer->pos; - token.value_len = 2; - lexer->pos += 2; - lexer->column += 2; - return token; - } - token.type = TOK_LT; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - } - - if (c == '>') { - if (lexer->pos + 1 < lexer->end && lexer->pos[1] == '=') { - token.type = TOK_GE; - token.value = lexer->pos; - token.value_len = 2; - lexer->pos += 2; - lexer->column += 2; - return token; - } - token.type = TOK_GT; - token.value = lexer->pos; - token.value_len = 1; - lexer->pos++; - lexer->column++; - return token; - } - - /* String literals */ - if (c == '\'' || c == '"') { - char quote = c; - const char* start = lexer->pos; - lexer->pos++; - lexer->column++; - - while (lexer->pos < lexer->end && *lexer->pos != quote) { - if (*lexer->pos == '\n') { - lexer->line++; - lexer->column = 1; - } else { - lexer->column++; - } - lexer->pos++; - } - - if (lexer->pos >= lexer->end) { - if (lexer->input && start >= lexer->input) { - size_t byte_offset = start - lexer->input; - - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_SYNTAX, - "Unterminated string literal", - lexer->input, - byte_offset, - token.line, - token.column - ); - } - - snprintf(lexer->error_msg, sizeof(lexer->error_msg), - "Unterminated string literal at line %d", token.line); - token.type = TOK_EOF; /* Error token */ - token.value = start; - token.value_len = 0; - return token; - } - - lexer->pos++; /* Skip closing quote */ - lexer->column++; - - token.type = TOK_STRING; - token.value = start; - token.value_len = lexer->pos - start; - return token; - } - - /* Numbers */ -parse_number: - if (isdigit((unsigned char)c) || c == '.') { - const char* start = lexer->pos; - - while (lexer->pos < lexer->end && - (isdigit((unsigned char)*lexer->pos) || *lexer->pos == '.')) { - lexer->pos++; - lexer->column++; - } - - token.type = TOK_NUMBER; - token.value = start; - token.value_len = lexer->pos - start; - return token; - } - - /* NCNames and QNames (and keywords) */ - if (is_ncname_start(c)) { - const char* start = lexer->pos; - - while (lexer->pos < lexer->end && is_ncname_char(*lexer->pos)) { - lexer->pos++; - lexer->column++; - } - - size_t len = lexer->pos - start; - - /* Check for QName (prefix:localname) */ - if (lexer->pos < lexer->end && *lexer->pos == ':' && - lexer->pos + 1 < lexer->end && lexer->pos[1] != ':') { /* Not :: */ - - lexer->pos++; /* Skip : */ - lexer->column++; - - if (is_ncname_start(*lexer->pos)) { - while (lexer->pos < lexer->end && is_ncname_char(*lexer->pos)) { - lexer->pos++; - lexer->column++; - } - - token.type = TOK_QNAME; - token.value = start; - token.value_len = lexer->pos - start; - return token; - } - } - - /* Check for keywords */ - token.type = check_keyword(start, len, 1, lexer); - token.value = start; - token.value_len = len; - return token; - } - - /* Invalid character */ - if (lexer->input && lexer->pos >= lexer->input) { - size_t byte_offset = lexer->pos - lexer->input; - char msg[256]; - snprintf(msg, sizeof(msg), - "Invalid character '%c' in XPath expression", - c); - - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_SYNTAX, - msg, - lexer->input, - byte_offset, - lexer->line, - lexer->column - ); - } - - snprintf(lexer->error_msg, sizeof(lexer->error_msg), - "Invalid character '%c' at line %d, column %d", - c, lexer->line, lexer->column); - token.type = TOK_EOF; /* Error token */ - token.value = lexer->pos; - token.value_len = 0; - return token; -} - -/* Get error message */ -const char* xpath_lexer_error(XPathLexer* lexer) { - return lexer ? lexer->error_msg : "NULL lexer"; -} - -/* Convert token type to string */ -const char* xpath_token_type_to_string(XPathTokenType type) { - if (type >= 0 && type < sizeof(xpath_token_type_names) / sizeof(xpath_token_type_names[0])) { - return xpath_token_type_names[type]; - } - return "UNKNOWN"; -} \ No newline at end of file diff --git a/ext/taurus/lib/src/xpath/lexer.h b/ext/taurus/lib/src/xpath/lexer.h deleted file mode 100644 index d044367..0000000 --- a/ext/taurus/lib/src/xpath/lexer.h +++ /dev/null @@ -1,25 +0,0 @@ -/* lexer.h - XPath lexer public API - * Copyright (c) 2024, Ribose Inc. - */ - -#ifndef XPATH_LEXER_H -#define XPATH_LEXER_H - -#include "xpath_internal.h" - -/* Create new lexer */ -XPathLexer* xpath_lexer_new(const char* input, size_t len); - -/* Free lexer resources */ -void xpath_lexer_free(XPathLexer* lexer); - -/* Get next token (returns token by value, value field points into input) */ -XPathToken xpath_lexer_next_token(XPathLexer* lexer); - -/* Get error message */ -const char* xpath_lexer_error(XPathLexer* lexer); - -/* Convert token type to string (for debugging) */ -const char* xpath_token_type_to_string(XPathTokenType type); - -#endif /* XPATH_LEXER_H */ \ No newline at end of file diff --git a/ext/taurus/lib/src/xpath/parser.c b/ext/taurus/lib/src/xpath/parser.c deleted file mode 100644 index 065c26e..0000000 --- a/ext/taurus/lib/src/xpath/parser.c +++ /dev/null @@ -1,1275 +0,0 @@ -/* parser.c - XPath parser implementation - * Copyright (c) 2024, Ribose Inc. - * - * Pure C implementation of XPath 1.0 parser. - * Uses token buffer for lookahead (matches taurus_internal.h). - */ - -#include "parser.h" -#include "lexer.h" -#include "../taurus_internal.h" -#include -#include -#include - -/* ============================================================================ - * Forward Declarations - * ============================================================================ */ - -/* Expression parsers (precedence order) */ -static XPathASTNode* parse_expr(XPathParser* parser); -static XPathASTNode* parse_or_expr(XPathParser* parser); -static XPathASTNode* parse_and_expr(XPathParser* parser); -static XPathASTNode* parse_equality_expr(XPathParser* parser); -static XPathASTNode* parse_relational_expr(XPathParser* parser); -static XPathASTNode* parse_additive_expr(XPathParser* parser); -static XPathASTNode* parse_multiplicative_expr(XPathParser* parser); -static XPathASTNode* parse_unary_expr(XPathParser* parser); -static XPathASTNode* parse_union_expr(XPathParser* parser); - -/* Path parsers */ -static XPathASTNode* parse_path_expr(XPathParser* parser); -static XPathASTNode* parse_filter_expr(XPathParser* parser); -static XPathASTNode* parse_primary_expr(XPathParser* parser); -static XPathASTNode* parse_location_path(XPathParser* parser); -static XPathASTNode* parse_relative_location_path(XPathParser* parser); -static XPathASTNode* parse_step(XPathParser* parser); - -/* Node test and predicate parsers */ -static XPathASTNode* parse_node_test(XPathParser* parser); -static XPathASTNode* parse_predicate(XPathParser* parser); -static XPathASTNode* parse_function_call(XPathParser* parser, const char* name, size_t name_len); - -/* Token management */ -static void advance_token(XPathParser* parser); -static XPathToken* peek_token(XPathParser* parser, int offset); -static XPathToken* current_token(XPathParser* parser); -static int current_token_is(XPathParser* parser, XPathTokenType type); -static int match_token(XPathParser* parser, XPathTokenType type); -static int consume_token(XPathParser* parser, XPathTokenType type, const char* error_msg); - -/* AST helpers */ -static char* token_to_string(const XPathToken* token); -static XPathASTNode* create_operator_node(XPathOperatorType op_type, - XPathASTNode* left, - XPathASTNode* right); - -/* ============================================================================ - * Parser Lifecycle - * ============================================================================ */ - -XPathParser* xpath_parser_new(const char* input, size_t len) { - if (!input) return NULL; - - XPathParser* parser = TAURUS_ALLOC(XPathParser); - if (!parser) return NULL; - - parser->lexer = xpath_lexer_new(input, len); - if (!parser->lexer) { - TAURUS_FREE(parser); - return NULL; - } - - parser->tokens = NULL; - parser->token_count = 0; - parser->token_pos = 0; - parser->error_msg[0] = '\0'; - - /* Tokenize entire input into array */ - size_t capacity = 16; - parser->tokens = TAURUS_ALLOC_N(XPathToken, capacity); - if (!parser->tokens) { - xpath_lexer_free(parser->lexer); - TAURUS_FREE(parser); - return NULL; - } - - /* Read all tokens */ - while (1) { - XPathToken tok = xpath_lexer_next_token(parser->lexer); - - /* Grow array if needed */ - if (parser->token_count >= capacity) { - capacity *= 2; - XPathToken* new_tokens = TAURUS_REALLOC_N(parser->tokens, XPathToken, capacity); - if (!new_tokens) { - TAURUS_FREE(parser->tokens); - xpath_lexer_free(parser->lexer); - TAURUS_FREE(parser); - return NULL; - } - parser->tokens = new_tokens; - } - - parser->tokens[parser->token_count++] = tok; - - if (tok.type == TOK_EOF) break; - } - - return parser; -} - -void xpath_parser_free(XPathParser* parser) { - if (!parser) return; - if (parser->tokens) { - TAURUS_FREE(parser->tokens); - } - if (parser->lexer) { - xpath_lexer_free(parser->lexer); - } - TAURUS_FREE(parser); -} - -const char* xpath_parser_error(XPathParser* parser) { - if (!parser) return "Invalid parser"; - return parser->error_msg[0] ? parser->error_msg : NULL; -} - -/* ============================================================================ - * AST Node Management - * ============================================================================ */ - -static XPathASTNode* ast_node_new(XPathASTType type) { - XPathASTNode* node = TAURUS_ALLOC(XPathASTNode); - if (!node) return NULL; - - node->type = type; - node->value = NULL; - node->number_value = 0.0; - node->children = NULL; - node->child_count = 0; - node->child_capacity = 0; - - /* Initialize namespace support fields (v0.8.0) */ - node->prefix = NULL; - node->local_name = NULL; - - return node; -} - -void ast_node_free(XPathASTNode* node) { - if (!node) return; - - if (node->value) { - TAURUS_FREE(node->value); - } - - /* Free namespace support fields (v0.8.0) */ - if (node->prefix) { - TAURUS_FREE(node->prefix); - } - if (node->local_name) { - TAURUS_FREE(node->local_name); - } - - if (node->children) { - for (size_t i = 0; i < node->child_count; i++) { - ast_node_free(node->children[i]); - } - TAURUS_FREE(node->children); - } - - TAURUS_FREE(node); -} - -static void ast_node_add_child(XPathASTNode* parent, XPathASTNode* child) { - if (!parent || !child) return; - - /* Resize if needed */ - if (parent->child_count >= parent->child_capacity) { - size_t new_capacity = parent->child_capacity == 0 ? 4 : parent->child_capacity * 2; - XPathASTNode** new_children = TAURUS_REALLOC_N(parent->children, XPathASTNode*, new_capacity); - if (!new_children) return; - parent->children = new_children; - parent->child_capacity = new_capacity; - } - - parent->children[parent->child_count++] = child; -} - -/* ============================================================================ - * Token Management - * ============================================================================ */ - -static XPathToken* current_token(XPathParser* parser) { - if (!parser || parser->token_pos >= parser->token_count) return NULL; - return &parser->tokens[parser->token_pos]; -} - -static void advance_token(XPathParser* parser) { - if (!parser) return; - if (parser->token_pos < parser->token_count) { - parser->token_pos++; - } -} - -static XPathToken* peek_token(XPathParser* parser, int offset) { - if (!parser) return NULL; - size_t pos = parser->token_pos + offset; - if (pos >= parser->token_count) return NULL; - return &parser->tokens[pos]; -} - -static int current_token_is(XPathParser* parser, XPathTokenType type) { - XPathToken* tok = current_token(parser); - return tok && (int)tok->type == (int)type; -} - -static int match_token(XPathParser* parser, XPathTokenType type) { - if (!parser) return 0; - if (current_token_is(parser, type)) { - advance_token(parser); - return 1; - } - return 0; -} - -static int consume_token(XPathParser* parser, XPathTokenType type, const char* error_msg) { - if (!parser) return 0; - XPathToken* tok = current_token(parser); - if (tok && (int)tok->type == (int)type) { - advance_token(parser); - return 1; - } - if (tok && parser->lexer && parser->lexer->input) { - /* Calculate byte offset from token position */ - size_t byte_offset = (tok->value && tok->value >= parser->lexer->input) - ? tok->value - parser->lexer->input - : 0; - - /* Build detailed error message */ - char detailed_msg[512]; - snprintf(detailed_msg, sizeof(detailed_msg), - "%s (got %s)", - error_msg, - xpath_token_type_to_string(tok->type)); - - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_SYNTAX, - detailed_msg, - parser->lexer->input, - byte_offset, - tok->line, - tok->column - ); - - /* Also store in parser for legacy compatibility */ - snprintf(parser->error_msg, sizeof(parser->error_msg), - "%s at line %d, column %d (got %s)", - error_msg, tok->line, tok->column, - xpath_token_type_to_string(tok->type)); - } else if (parser->lexer && parser->lexer->input) { - size_t byte_offset = (parser->lexer->end && parser->lexer->end >= parser->lexer->input) - ? parser->lexer->end - parser->lexer->input - : 0; - - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_SYNTAX, - error_msg, - parser->lexer->input, - byte_offset, - parser->lexer->line, - parser->lexer->column - ); - - snprintf(parser->error_msg, sizeof(parser->error_msg), - "%s at EOF", error_msg); - } else { - /* Fallback: set error without context */ - snprintf(parser->error_msg, sizeof(parser->error_msg), - "%s", error_msg); - } - return 0; -} - -/* ============================================================================ - * Helper Functions - * ============================================================================ */ - -static char* token_to_string(const XPathToken* token) { - if (!token || token->value_len == 0) return NULL; - - char* str = TAURUS_ALLOC_N(char, token->value_len + 1); - if (!str) return NULL; - - memcpy(str, token->value, token->value_len); - str[token->value_len] = '\0'; - return str; -} - -static XPathASTNode* create_operator_node(XPathOperatorType op_type, - XPathASTNode* left, - XPathASTNode* right) { - XPathASTNode* node = ast_node_new(XPATH_AST_OPERATOR); - if (!node) { - ast_node_free(left); - ast_node_free(right); - return NULL; - } - - node->number_value = (double)op_type; - ast_node_add_child(node, left); - ast_node_add_child(node, right); - return node; -} - -/* ============================================================================ - * Main Parser Entry Point - * ============================================================================ */ - -static XPathASTNode* parse_expr(XPathParser* parser) { - return parse_or_expr(parser); -} - -XPathASTNode* xpath_parse(XPathParser* parser) { - if (!parser) return NULL; - - XPathASTNode* ast = parse_expr(parser); - - if (ast && !current_token_is(parser, TOK_EOF)) { - XPathToken* tok = current_token(parser); - if (tok && parser->lexer && parser->lexer->input) { - size_t byte_offset = (tok->value && tok->value >= parser->lexer->input) - ? tok->value - parser->lexer->input - : 0; - char msg[256]; - snprintf(msg, sizeof(msg), - "Unexpected token after expression: %s", - xpath_token_type_to_string(tok->type)); - - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_SYNTAX, - msg, - parser->lexer->input, - byte_offset, - tok->line, - tok->column - ); - - snprintf(parser->error_msg, sizeof(parser->error_msg), - "Unexpected token after expression: %s at line %d, column %d", - xpath_token_type_to_string(tok->type), - tok->line, tok->column); - } else { - snprintf(parser->error_msg, sizeof(parser->error_msg), - "Unexpected token after expression"); - } - ast_node_free(ast); - return NULL; - } - - return ast; -} - -/* ============================================================================ - * Expression Parsers (Operator Precedence) - * ============================================================================ */ - -/* Parse OR expressions: AndExpr ( 'or' AndExpr )* */ -static XPathASTNode* parse_or_expr(XPathParser* parser) { - if (!parser) return NULL; - - XPathASTNode* left = parse_and_expr(parser); - if (!left) return NULL; - - while (current_token_is(parser, TOK_OR)) { - advance_token(parser); - XPathASTNode* right = parse_and_expr(parser); - if (!right) { - ast_node_free(left); - return NULL; - } - - left = create_operator_node(XPATH_OP_OR, left, right); - if (!left) return NULL; - } - - return left; -} - -/* Parse AND expressions: EqualityExpr ( 'and' EqualityExpr )* */ -static XPathASTNode* parse_and_expr(XPathParser* parser) { - if (!parser) return NULL; - - XPathASTNode* left = parse_equality_expr(parser); - if (!left) return NULL; - - while (current_token_is(parser, TOK_AND)) { - advance_token(parser); - XPathASTNode* right = parse_equality_expr(parser); - if (!right) { - ast_node_free(left); - return NULL; - } - - left = create_operator_node(XPATH_OP_AND, left, right); - if (!left) return NULL; - } - - return left; -} - -/* Parse equality expressions: RelationalExpr ( ('=' | '!=') RelationalExpr )* */ -static XPathASTNode* parse_equality_expr(XPathParser* parser) { - if (!parser) return NULL; - - XPathASTNode* left = parse_relational_expr(parser); - if (!left) return NULL; - - while (1) { - XPathOperatorType op_type; - - if (current_token_is(parser, TOK_EQUALS)) { - op_type = XPATH_OP_EQUAL; - } else if (current_token_is(parser, TOK_NOT_EQUALS)) { - op_type = XPATH_OP_NOT_EQUAL; - } else { - break; - } - - advance_token(parser); - XPathASTNode* right = parse_relational_expr(parser); - if (!right) { - ast_node_free(left); - return NULL; - } - - left = create_operator_node(op_type, left, right); - if (!left) return NULL; - } - - return left; -} - -/* Parse relational expressions: AdditiveExpr ( ('<' | '>' | '<=' | '>=') AdditiveExpr )* */ -static XPathASTNode* parse_relational_expr(XPathParser* parser) { - XPathASTNode* left = parse_additive_expr(parser); - if (!left) return NULL; - - while (1) { - XPathOperatorType op_type; - - if (current_token_is(parser, TOK_LT)) { - op_type = XPATH_OP_LESS; - } else if (current_token_is(parser, TOK_LE)) { - op_type = XPATH_OP_LESS_EQUAL; - } else if (current_token_is(parser, TOK_GT)) { - op_type = XPATH_OP_GREATER; - } else if (current_token_is(parser, TOK_GE)) { - op_type = XPATH_OP_GREATER_EQUAL; - } else { - break; - } - - advance_token(parser); - XPathASTNode* right = parse_additive_expr(parser); - if (!right) { - ast_node_free(left); - return NULL; - } - - left = create_operator_node(op_type, left, right); - if (!left) return NULL; - } - - return left; -} - -/* Parse additive expressions: MultiplicativeExpr ( ('+' | '-') MultiplicativeExpr )* */ -static XPathASTNode* parse_additive_expr(XPathParser* parser) { - XPathASTNode* left = parse_multiplicative_expr(parser); - if (!left) return NULL; - - while (1) { - XPathOperatorType op_type; - - if (current_token_is(parser, TOK_PLUS)) { - op_type = XPATH_OP_PLUS; - } else if (current_token_is(parser, TOK_MINUS)) { - op_type = XPATH_OP_MINUS; - } else { - break; - } - - advance_token(parser); - XPathASTNode* right = parse_multiplicative_expr(parser); - if (!right) { - ast_node_free(left); - return NULL; - } - - left = create_operator_node(op_type, left, right); - if (!left) return NULL; - } - - return left; -} - -/* Parse multiplicative expressions: UnaryExpr ( ('*' | 'div' | 'mod') UnaryExpr )* */ -static XPathASTNode* parse_multiplicative_expr(XPathParser* parser) { - XPathASTNode* left = parse_unary_expr(parser); - if (!left) return NULL; - - while (1) { - XPathOperatorType op_type; - - if (current_token_is(parser, TOK_STAR)) { - op_type = XPATH_OP_MULTIPLY; - } else if (current_token_is(parser, TOK_DIV)) { - op_type = XPATH_OP_DIV; - } else if (current_token_is(parser, TOK_MOD)) { - op_type = XPATH_OP_MOD; - } else { - break; - } - - advance_token(parser); - XPathASTNode* right = parse_unary_expr(parser); - if (!right) { - ast_node_free(left); - return NULL; - } - - left = create_operator_node(op_type, left, right); - if (!left) return NULL; - } - - return left; -} - -/* Parse unary expressions: '-' UnaryExpr | UnionExpr */ -static XPathASTNode* parse_unary_expr(XPathParser* parser) { - if (current_token_is(parser, TOK_MINUS)) { - advance_token(parser); - XPathASTNode* expr = parse_unary_expr(parser); - if (!expr) return NULL; - - XPathASTNode* node = ast_node_new(XPATH_AST_OPERATOR); - if (!node) { - ast_node_free(expr); - return NULL; - } - - node->number_value = (double)XPATH_OP_NEGATION; - ast_node_add_child(node, expr); - return node; - } - - return parse_union_expr(parser); -} - -/* Parse union expressions: PathExpr ( '|' PathExpr )* */ -static XPathASTNode* parse_union_expr(XPathParser* parser) { - XPathASTNode* left = parse_path_expr(parser); - if (!left) return NULL; - - while (current_token_is(parser, TOK_PIPE)) { - advance_token(parser); - XPathASTNode* right = parse_path_expr(parser); - if (!right) { - ast_node_free(left); - return NULL; - } - - left = create_operator_node(XPATH_OP_UNION, left, right); - if (!left) return NULL; - } - - return left; -} - -/* ============================================================================ - * Path Parsers - * ============================================================================ */ - -/* Parse path expressions */ -static XPathASTNode* parse_path_expr(XPathParser* parser) { - /* Check if it starts with location path indicator */ - if (current_token_is(parser, TOK_SLASH) || - current_token_is(parser, TOK_DOUBLE_SLASH) || - current_token_is(parser, TOK_AT) || - current_token_is(parser, TOK_DOT) || - current_token_is(parser, TOK_DOUBLE_DOT) || - current_token_is(parser, TOK_STAR) || - (current_token(parser) && current_token(parser)->type >= TOK_ANCESTOR && - current_token(parser)->type <= TOK_SELF)) { - return parse_location_path(parser); - } - - /* Check for relative paths starting with NCName/QName */ - if (current_token_is(parser, TOK_NCNAME) || current_token_is(parser, TOK_QNAME)) { - XPathToken* next = peek_token(parser, 1); - - /* If followed by '(', it's a function call */ - if (next && next->type != TOK_LPAREN) { - return parse_location_path(parser); - } - } - - /* Try filter expression */ - XPathASTNode* expr = parse_filter_expr(parser); - if (!expr) return NULL; - - /* Check for path continuation */ - if (current_token_is(parser, TOK_SLASH) || current_token_is(parser, TOK_DOUBLE_SLASH)) { - XPathASTNode* path = ast_node_new(XPATH_AST_PATH_EXPR); - if (!path) { - ast_node_free(expr); - return NULL; - } - - ast_node_add_child(path, expr); - - int is_double = current_token_is(parser, TOK_DOUBLE_SLASH); - advance_token(parser); - - XPathASTNode* rel_path = parse_relative_location_path(parser); - if (!rel_path) { - ast_node_free(path); - return NULL; - } - - if (is_double) { - XPathASTNode* desc_step = ast_node_new(XPATH_AST_STEP); - if (desc_step) { - desc_step->value = taurus_strdup("descendant-or-self"); - XPathASTNode* node_test = ast_node_new(XPATH_AST_NODE_TEST_TYPE); - if (node_test) { - node_test->value = taurus_strdup("node"); - } - ast_node_add_child(desc_step, node_test); - ast_node_add_child(path, desc_step); - } - } - - ast_node_add_child(path, rel_path); - return path; - } - - return expr; -} - -/* Parse filter expressions */ -static XPathASTNode* parse_filter_expr(XPathParser* parser) { - XPathASTNode* expr = parse_primary_expr(parser); - if (!expr) return NULL; - - /* Parse predicates */ - while (current_token_is(parser, TOK_LBRACKET)) { - XPathASTNode* pred = parse_predicate(parser); - if (!pred) { - ast_node_free(expr); - return NULL; - } - - XPathASTNode* filter = ast_node_new(XPATH_AST_PREDICATE); - if (!filter) { - ast_node_free(expr); - ast_node_free(pred); - return NULL; - } - - ast_node_add_child(filter, expr); - ast_node_add_child(filter, pred); - expr = filter; - } - - return expr; -} - -/* Parse primary expressions: NUMBER | STRING | FunctionCall | '(' Expr ')' */ -static XPathASTNode* parse_primary_expr(XPathParser* parser) { - XPathToken* tok = current_token(parser); - if (!tok) { - if (parser->lexer && parser->lexer->input) { - size_t byte_offset = (parser->lexer->end && parser->lexer->end >= parser->lexer->input) - ? parser->lexer->end - parser->lexer->input - : 0; - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_SYNTAX, - "Unexpected end of XPath expression", - parser->lexer->input, - byte_offset, - parser->lexer->line, - parser->lexer->column - ); - } - - snprintf(parser->error_msg, sizeof(parser->error_msg), - "Unexpected EOF in primary expression"); - return NULL; - } - - /* Number literal */ - if (tok->type == TOK_NUMBER) { - XPathASTNode* node = ast_node_new(XPATH_AST_NUMBER); - if (!node) return NULL; - - char* num_str = token_to_string(tok); - if (num_str) { - node->number_value = strtod(num_str, NULL); - TAURUS_FREE(num_str); - } - advance_token(parser); - return node; - } - - /* String literal */ - if (tok->type == TOK_STRING) { - XPathASTNode* node = ast_node_new(XPATH_AST_STRING); - if (!node) return NULL; - - /* Remove quotes */ - if (tok->value_len >= 2) { - size_t len = tok->value_len - 2; - node->value = TAURUS_ALLOC_N(char, len + 1); - if (node->value) { - memcpy(node->value, tok->value + 1, len); - node->value[len] = '\0'; - } - } - advance_token(parser); - return node; - } - - /* Parenthesized expression */ - if (tok->type == TOK_LPAREN) { - advance_token(parser); - XPathASTNode* expr = parse_expr(parser); - if (!expr) return NULL; - - if (!consume_token(parser, TOK_RPAREN, "Expected ')' after expression")) { - ast_node_free(expr); - return NULL; - } - return expr; - } - - /* Function call with node type tokens */ - if (tok->type >= TOK_COMMENT && tok->type <= TOK_NODE) { - XPathToken name_token = *tok; - XPathToken* next = peek_token(parser, 1); - - if (next && next->type == TOK_LPAREN) { - advance_token(parser); - return parse_function_call(parser, name_token.value, name_token.value_len); - } - } - - /* Function call with NCName/QName */ - if (tok->type == TOK_NCNAME || tok->type == TOK_QNAME) { - XPathToken name_token = *tok; - XPathToken* next = peek_token(parser, 1); - - if (next && next->type == TOK_LPAREN) { - advance_token(parser); - return parse_function_call(parser, name_token.value, name_token.value_len); - } - } - - if (parser->lexer && parser->lexer->input && tok->value) { - size_t byte_offset = (tok->value >= parser->lexer->input) - ? tok->value - parser->lexer->input - : 0; - char msg[256]; - snprintf(msg, sizeof(msg), - "Unexpected token in primary expression: %s", - xpath_token_type_to_string(tok->type)); - - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_SYNTAX, - msg, - parser->lexer->input, - byte_offset, - tok->line, - tok->column - ); - } - - snprintf(parser->error_msg, sizeof(parser->error_msg), - "Unexpected token %s at line %d, column %d in primary expression", - xpath_token_type_to_string(tok->type), - tok->line, tok->column); - return NULL; -} - -/* Parse function call */ -static XPathASTNode* parse_function_call(XPathParser* parser, const char* name, size_t name_len) { - XPathASTNode* node = ast_node_new(XPATH_AST_FUNCTION_CALL); - if (!node) return NULL; - - node->value = TAURUS_ALLOC_N(char, name_len + 1); - if (node->value) { - memcpy(node->value, name, name_len); - node->value[name_len] = '\0'; - } - - if (!consume_token(parser, TOK_LPAREN, "Expected '(' after function name")) { - ast_node_free(node); - return NULL; - } - - /* Parse arguments */ - if (!current_token_is(parser, TOK_RPAREN)) { - do { - XPathASTNode* arg = parse_expr(parser); - if (!arg) { - ast_node_free(node); - return NULL; - } - ast_node_add_child(node, arg); - } while (match_token(parser, TOK_COMMA)); - } - - if (!consume_token(parser, TOK_RPAREN, "Expected ')' after function arguments")) { - ast_node_free(node); - return NULL; - } - - return node; -} - -/* ============================================================================ - * Location Path Parsers - * ============================================================================ */ - -/* Parse location path */ -static XPathASTNode* parse_location_path(XPathParser* parser) { - XPathASTNode* node; - - /* Absolute path starting with / */ - if (current_token_is(parser, TOK_SLASH)) { - advance_token(parser); - - node = ast_node_new(XPATH_AST_ABSOLUTE_PATH); - if (!node) return NULL; - - /* If followed by a step, parse relative path */ - if (!current_token_is(parser, TOK_EOF) && - !current_token_is(parser, TOK_RPAREN) && - !current_token_is(parser, TOK_RBRACKET) && - !current_token_is(parser, TOK_PIPE)) { - - XPathASTNode* rel = parse_relative_location_path(parser); - if (!rel) { - ast_node_free(node); - return NULL; - } - ast_node_add_child(node, rel); - } - - return node; - } - - /* Absolute path starting with // */ - if (current_token_is(parser, TOK_DOUBLE_SLASH)) { - advance_token(parser); - - node = ast_node_new(XPATH_AST_ABSOLUTE_PATH); - if (!node) return NULL; - - /* Optimization: double-slash followed by star should be a single descendant-or-self::* step, - * not two steps (descendant-or-self::node() + child::*). - * This matches how most XPath implementations handle double-slash-star for efficiency. */ - if (current_token_is(parser, TOK_STAR)) { - /* Create single step: descendant-or-self::* */ - XPathASTNode* desc_step = ast_node_new(XPATH_AST_STEP); - if (!desc_step) { - ast_node_free(node); - return NULL; - } - desc_step->value = taurus_strdup("descendant-or-self"); - - /* Use wildcard node test instead of node() */ - XPathASTNode* node_test = ast_node_new(XPATH_AST_NODE_TEST_ALL); - if (!node_test) { - ast_node_free(desc_step); - ast_node_free(node); - return NULL; - } - ast_node_add_child(desc_step, node_test); - - /* Consume the * token */ - advance_token(parser); - - /* Parse predicates (FIX: was missing before!) */ - while (current_token_is(parser, TOK_LBRACKET)) { - XPathASTNode* pred = parse_predicate(parser); - if (!pred) { - ast_node_free(desc_step); - ast_node_free(node); - return NULL; - } - ast_node_add_child(desc_step, pred); - } - - ast_node_add_child(node, desc_step); - - return node; - } - - /* General case: Add descendant-or-self::node() step */ - XPathASTNode* desc_step = ast_node_new(XPATH_AST_STEP); - if (!desc_step) { - ast_node_free(node); - return NULL; - } - - desc_step->value = taurus_strdup("descendant-or-self"); - XPathASTNode* node_test = ast_node_new(XPATH_AST_NODE_TEST_TYPE); - if (!node_test) { - ast_node_free(desc_step); - ast_node_free(node); - return NULL; - } - node_test->value = taurus_strdup("node"); - ast_node_add_child(desc_step, node_test); - ast_node_add_child(node, desc_step); - - /* Parse relative path */ - XPathASTNode* rel = parse_relative_location_path(parser); - if (!rel) { - ast_node_free(node); - return NULL; - } - ast_node_add_child(node, rel); - - return node; - } - - /* Relative path */ - XPathASTNode* rel = parse_relative_location_path(parser); - if (!rel) return NULL; - - /* Unwrap single-step relative paths */ - if (rel->type == XPATH_AST_RELATIVE_PATH && rel->child_count == 1) { - XPathASTNode* single_step = rel->children[0]; - rel->children[0] = NULL; - rel->child_count = 0; - ast_node_free(rel); - return single_step; - } - - return rel; -} - -/* Parse relative location path */ -static XPathASTNode* parse_relative_location_path(XPathParser* parser) { - XPathASTNode* node = ast_node_new(XPATH_AST_RELATIVE_PATH); - if (!node) return NULL; - - /* Parse first step */ - XPathASTNode* step = parse_step(parser); - if (!step) { - ast_node_free(node); - return NULL; - } - ast_node_add_child(node, step); - - /* Parse additional steps */ - while (current_token_is(parser, TOK_SLASH) || current_token_is(parser, TOK_DOUBLE_SLASH)) { - int is_double = current_token_is(parser, TOK_DOUBLE_SLASH); - advance_token(parser); - - if (is_double) { - /* Insert descendant-or-self::node() step */ - XPathASTNode* desc_step = ast_node_new(XPATH_AST_STEP); - if (!desc_step) { - ast_node_free(node); - return NULL; - } - desc_step->value = taurus_strdup("descendant-or-self"); - XPathASTNode* node_test = ast_node_new(XPATH_AST_NODE_TEST_TYPE); - if (node_test) { - node_test->value = taurus_strdup("node"); - } - ast_node_add_child(desc_step, node_test); - ast_node_add_child(node, desc_step); - } - - step = parse_step(parser); - if (!step) { - ast_node_free(node); - return NULL; - } - ast_node_add_child(node, step); - } - - return node; -} - -/* Parse step */ -static XPathASTNode* parse_step(XPathParser* parser) { - /* Handle abbreviated steps */ - if (current_token_is(parser, TOK_DOT)) { - advance_token(parser); - XPathASTNode* node = ast_node_new(XPATH_AST_STEP); - if (node) { - node->value = taurus_strdup("self"); - XPathASTNode* test = ast_node_new(XPATH_AST_NODE_TEST_ALL); - ast_node_add_child(node, test); - } - return node; - } - - if (current_token_is(parser, TOK_DOUBLE_DOT)) { - advance_token(parser); - XPathASTNode* node = ast_node_new(XPATH_AST_STEP); - if (node) { - node->value = taurus_strdup("parent"); - XPathASTNode* test = ast_node_new(XPATH_AST_NODE_TEST_ALL); - ast_node_add_child(node, test); - } - return node; - } - - /* Handle @ abbreviation */ - if (current_token_is(parser, TOK_AT)) { - advance_token(parser); - XPathASTNode* node = ast_node_new(XPATH_AST_STEP); - if (!node) return NULL; - - node->value = taurus_strdup("attribute"); - - XPathASTNode* test = parse_node_test(parser); - if (!test) { - ast_node_free(node); - return NULL; - } - ast_node_add_child(node, test); - - /* Parse predicates */ - while (current_token_is(parser, TOK_LBRACKET)) { - XPathASTNode* pred = parse_predicate(parser); - if (!pred) { - ast_node_free(node); - return NULL; - } - ast_node_add_child(node, pred); - } - - return node; - } - - XPathASTNode* node = ast_node_new(XPATH_AST_STEP); - if (!node) return NULL; - - /* Check for axis specifier */ - char* axis = NULL; - XPathToken* tok = current_token(parser); - - if (tok && tok->type >= TOK_ANCESTOR && tok->type <= TOK_SELF) { - axis = token_to_string(tok); - advance_token(parser); - - if (!consume_token(parser, TOK_DOUBLE_COLON, "Expected '::' after axis name")) { - if (axis) TAURUS_FREE(axis); - ast_node_free(node); - return NULL; - } - } - - node->value = axis ? taurus_strdup(axis) : taurus_strdup("child"); - if (axis) TAURUS_FREE(axis); - - /* Parse node test */ - XPathASTNode* test = parse_node_test(parser); - if (!test) { - ast_node_free(node); - return NULL; - } - ast_node_add_child(node, test); - - /* Parse predicates */ - while (current_token_is(parser, TOK_LBRACKET)) { - XPathASTNode* pred = parse_predicate(parser); - if (!pred) { - ast_node_free(node); - return NULL; - } - ast_node_add_child(node, pred); - } - - return node; -} - -/* ============================================================================ - * Node Test and Predicate Parsers - * ============================================================================ */ - -/* Parse node test */ -static XPathASTNode* parse_node_test(XPathParser* parser) { - XPathToken* tok = current_token(parser); - if (!tok) { - if (parser->lexer && parser->lexer->input) { - size_t byte_offset = (parser->lexer->end && parser->lexer->end >= parser->lexer->input) - ? parser->lexer->end - parser->lexer->input - : 0; - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_SYNTAX, - "Expected node test, got end of expression", - parser->lexer->input, - byte_offset, - parser->lexer->line, - parser->lexer->column - ); - } - - snprintf(parser->error_msg, sizeof(parser->error_msg), - "Expected node test at EOF"); - return NULL; - } - - /* Node type tests */ - if (tok->type == TOK_COMMENT || tok->type == TOK_TEXT || - tok->type == TOK_NODE || tok->type == TOK_PROCESSING_INSTRUCTION) { - - XPathASTNode* node = ast_node_new(XPATH_AST_NODE_TEST_TYPE); - if (!node) return NULL; - - node->value = token_to_string(tok); - advance_token(parser); - - if (!consume_token(parser, TOK_LPAREN, "Expected '(' after node type")) { - ast_node_free(node); - return NULL; - } - - if (current_token_is(parser, TOK_STRING)) { - char* arg = token_to_string(current_token(parser)); - if (arg) TAURUS_FREE(arg); - advance_token(parser); - } - - if (!consume_token(parser, TOK_RPAREN, "Expected ')' after node type")) { - ast_node_free(node); - return NULL; - } - - return node; - } - - /* Check for namespace wildcard: prefix:* - * Lexer produces: TOK_NCNAME("prefix") followed by TOK_STAR - * when it sees prefix:* because * is not an ncname_start char */ - if (tok->type == TOK_NCNAME) { - XPathToken* next = peek_token(parser, 1); - - /* Check if next token is * (namespace wildcard pattern) */ - if (next && next->type == TOK_STAR) { - /* This is prefix:* pattern */ - XPathASTNode* node = ast_node_new(XPATH_AST_NODE_TEST_ALL); - if (!node) return NULL; - - /* Set prefix from NCName token */ - node->prefix = token_to_string(tok); - node->value = taurus_strdup("*"); - - advance_token(parser); /* Consume NCName */ - advance_token(parser); /* Consume STAR */ - - return node; - } - /* Otherwise fall through to normal name test handling below */ - } - - /* Wildcard */ - if (tok->type == TOK_STAR) { - advance_token(parser); - return ast_node_new(XPATH_AST_NODE_TEST_ALL); - } - - /* Name test */ - if (tok->type == TOK_NCNAME || tok->type == TOK_QNAME) { - XPathASTNode* node = ast_node_new(XPATH_AST_NODE_TEST_NAME); - if (!node) return NULL; - - /* Get full QName string */ - char* full_name = token_to_string(tok); - if (!full_name) { - ast_node_free(node); - return NULL; - } - - /* Split into prefix and local name (v0.8.0 namespace support) */ - char* colon = strchr(full_name, ':'); - if (colon && tok->type == TOK_QNAME) { - /* Has namespace prefix */ - size_t prefix_len = colon - full_name; - node->prefix = TAURUS_ALLOC_N(char, prefix_len + 1); - if (node->prefix) { - memcpy(node->prefix, full_name, prefix_len); - node->prefix[prefix_len] = '\0'; - } - node->local_name = taurus_strdup(colon + 1); - } else { - /* No prefix - simple NCName */ - node->prefix = NULL; - node->local_name = taurus_strdup(full_name); - } - - node->value = full_name; /* Keep full name for backward compat */ - advance_token(parser); - - return node; - } - - /* v1.1.0: Allow operator keywords as element names in node tests - * This fixes axis::name syntax where name happens to be a keyword - * e.g., ancestor::div, child::mod, parent::and, self::or */ - if (tok->type == TOK_DIV || tok->type == TOK_MOD || - tok->type == TOK_AND || tok->type == TOK_OR) { - XPathASTNode* node = ast_node_new(XPATH_AST_NODE_TEST_NAME); - if (!node) return NULL; - - char* name = token_to_string(tok); - if (!name) { - ast_node_free(node); - return NULL; - } - - node->prefix = NULL; - node->local_name = taurus_strdup(name); - node->value = name; - advance_token(parser); - - return node; - } - - if (parser->lexer && parser->lexer->input && tok->value) { - size_t byte_offset = (tok->value >= parser->lexer->input) - ? tok->value - parser->lexer->input - : 0; - - taurus_set_error_with_context( - TAURUS_ERROR_XPATH_SYNTAX, - "Expected node test", - parser->lexer->input, - byte_offset, - tok->line, - tok->column - ); - } - - snprintf(parser->error_msg, sizeof(parser->error_msg), - "Expected node test at line %d, column %d", - tok->line, tok->column); - return NULL; -} - -/* Parse predicate */ -static XPathASTNode* parse_predicate(XPathParser* parser) { - if (!consume_token(parser, TOK_LBRACKET, "Expected '[' to start predicate")) { - return NULL; - } - - XPathASTNode* expr = parse_expr(parser); - if (!expr) return NULL; - - if (!consume_token(parser, TOK_RBRACKET, "Expected ']' to end predicate")) { - ast_node_free(expr); - return NULL; - } - - return expr; -} \ No newline at end of file diff --git a/ext/taurus/lib/src/xpath/parser.h b/ext/taurus/lib/src/xpath/parser.h deleted file mode 100644 index 18a7925..0000000 --- a/ext/taurus/lib/src/xpath/parser.h +++ /dev/null @@ -1,28 +0,0 @@ -/* parser.h - XPath parser public API - * Copyright (c) 2024, Ribose Inc. - */ - -#ifndef XPATH_PARSER_H -#define XPATH_PARSER_H - -#include "xpath_internal.h" - -/* Forward declarations - types already defined in taurus_internal.h */ -/* No need to redefine them here */ - -/* Create new parser */ -XPathParser* xpath_parser_new(const char* input, size_t len); - -/* Free parser resources */ -void xpath_parser_free(XPathParser* parser); - -/* Parse expression into AST (caller owns returned node) */ -XPathASTNode* xpath_parse(XPathParser* parser); - -/* Get error message (returns NULL if no error) */ -const char* xpath_parser_error(XPathParser* parser); - -/* Free AST node and all children recursively */ -void ast_node_free(XPathASTNode* node); - -#endif /* XPATH_PARSER_H */ \ No newline at end of file diff --git a/ext/taurus/lib/src/xpath/xpath_internal.h b/ext/taurus/lib/src/xpath/xpath_internal.h deleted file mode 100644 index 08c20be..0000000 --- a/ext/taurus/lib/src/xpath/xpath_internal.h +++ /dev/null @@ -1,68 +0,0 @@ -/* xpath_internal.h - Internal XPath structures - * Copyright (c) 2024, Ribose Inc. - * INTERNAL HEADER - Not part of public API - */ - -#ifndef XPATH_INTERNAL_H -#define XPATH_INTERNAL_H - -#include "../taurus_internal.h" - -/* XPath token types - Complete set from ext/taurus/lexer_xpath.c */ -typedef enum { - TOK_EOF = 0, - TOK_SLASH, - TOK_DOUBLE_SLASH, - TOK_AT, - TOK_DOT, - TOK_DOUBLE_DOT, - TOK_LPAREN, - TOK_RPAREN, - TOK_LBRACKET, - TOK_RBRACKET, - TOK_COMMA, - TOK_DOUBLE_COLON, - TOK_NCNAME, - TOK_QNAME, - TOK_STRING, - TOK_NUMBER, - TOK_EQUALS, - TOK_NOT_EQUALS, - TOK_LT, - TOK_LE, - TOK_GT, - TOK_GE, - TOK_PLUS, - TOK_MINUS, - TOK_STAR, - TOK_PIPE, - TOK_AND, - TOK_OR, - TOK_DIV, - TOK_MOD, - TOK_ANCESTOR, - TOK_ANCESTOR_OR_SELF, - TOK_ATTRIBUTE, - TOK_CHILD, - TOK_DESCENDANT, - TOK_DESCENDANT_OR_SELF, - TOK_FOLLOWING, - TOK_FOLLOWING_SIBLING, - TOK_NAMESPACE, - TOK_PARENT, - TOK_PRECEDING, - TOK_PRECEDING_SIBLING, - TOK_SELF, - TOK_COMMENT, - TOK_TEXT, - TOK_PROCESSING_INSTRUCTION, - TOK_NODE -} XPathTokenType; - -/* Token type names for debugging */ -extern const char* xpath_token_type_names[]; - -/* Use XPathToken and XPathLexer types from taurus_internal.h */ -/* They are already defined there, no need to redefine */ - -#endif /* XPATH_INTERNAL_H */ \ No newline at end of file diff --git a/lib/libtaurus.dylib b/lib/libtaurus.dylib deleted file mode 100755 index dfd0c30..0000000 Binary files a/lib/libtaurus.dylib and /dev/null differ diff --git a/lib/taurus.rb b/lib/taurus.rb index 9957868..614ad67 100644 --- a/lib/taurus.rb +++ b/lib/taurus.rb @@ -1,250 +1,7 @@ # frozen_string_literal: true -# Load Ruby classes first -require_relative "taurus/version" -require_relative "taurus/node" -require_relative "taurus/element" -require_relative "taurus/document" -require_relative "taurus/node_set" -require_relative "taurus/xpath" +require "taurus/version" -# Load FFI modules -require_relative "taurus/ffi/library" -require_relative "taurus/ffi/types" -require_relative "taurus/ffi/memory" -require_relative "taurus/ffi/errors" -require_relative "taurus/ffi/bridge" - -# Taurus is a high-performance XML parser for Ruby with complete XML Namespaces 1.0 -# and XPath 1.0 support. It provides Ox-level parsing speed with full features -# implemented in C for maximum performance. -# -# @example Basic parsing -# doc = Taurus.parse('content') -# root = doc.root -# root.name # => "root" -# root.nodes.first[:id] # => "1" -# -# @example Parsing from file -# doc = Taurus.parse_file('document.xml') -# -# @example XPath queries -# doc = Taurus.parse('Ruby') -# titles = doc.xpath('//title') -# count = doc.xpath('count(//book)') # => 1.0 -# -# @example Working with namespaces -# xml = '' -# doc = Taurus.parse(xml) -# doc.root.namespace[:href] # => "http://example.org" -# -# @see Document -# @see Element -# @see XPath module Taurus - # Base error class for Taurus library - class Error < StandardError; end - - # Raised when XML parsing fails due to malformed input - # - # @example - # Taurus.parse('') # raises Taurus::ParseError - class ParseError < Error - # @return [Symbol] error code symbol - attr_reader :code - - # @return [Integer] line number where error occurred (1-based, 0 if not available) - attr_reader :line - - # @return [Integer] column number where error occurred (1-based, 0 if not available) - attr_reader :column - - # @return [Integer] byte offset where error occurred (0 if not available) - attr_reader :byte_offset - - # @return [String, nil] context snippet showing code around error - attr_reader :context - - def initialize(message, code: :parse_failed, line: 0, column: 0, byte_offset: 0, context: nil) - super(message) - @code = code - @line = line - @column = column - @byte_offset = byte_offset - @context = context - end - end - - # Raised when XPath evaluation fails due to syntax or semantic errors - # - # @example - # doc.xpath('//book:title') # raises Taurus::XPathError if 'book' prefix not registered - class XPathError < Error - attr_reader :code, :line, :column, :byte_offset, :context - - def initialize(message, code: :xpath_error, line: 0, column: 0, byte_offset: 0, context: nil) - super(message) - @code = code - @line = line - @column = column - @byte_offset = byte_offset - @context = context - end - end - - # Raised when XPath evaluation fails due to runtime issues - # - # @example - # doc.xpath('sum("not a number")') # raises Taurus::EvaluationError - class EvaluationError < Error - attr_reader :code, :line, :column, :byte_offset, :context - - def initialize(message, code: :eval_error, line: 0, column: 0, byte_offset: 0, context: nil) - super(message) - @code = code - @line = line - @column = column - @byte_offset = byte_offset - @context = context - end - end - - class << self - # Parse an XML string and return a Document - # - # Parses the given XML string using the native libtaurus C library via FFI. - # Returns a fully constructed Ruby Document object with all elements, - # attributes, and namespace information. - # - # @param xml_string [String] the XML string to parse - # @return [Document] the parsed document - # @raise [ParseError] if XML is malformed - # - # @example - # doc = Taurus.parse('content') - # doc.root.name # => "root" - # doc.root.nodes.first[:id] # => "1" - # - # @see .parse_file - def parse(xml_string) - # Validate input in Ruby layer first - if xml_string.nil? - raise ParseError.new( - "NULL input provided", - code: :null_input, - line: 0, - column: 0 - ) - end - - if xml_string.empty? - raise ParseError.new( - "Empty input provided", - code: :empty_input, - line: 0, - column: 0 - ) - end - - FFI::ErrorHandling.with_error_check do - # Call C library to parse XML - doc_ptr = FFI.taurus_parse(xml_string, xml_string.bytesize) - - if doc_ptr.null? - FFI::ErrorHandling.check_error! - raise ParseError, "Failed to parse XML" - end - - # Wrap pointer for automatic cleanup - doc_ptr = FFI::MemoryHelpers.wrap_document(doc_ptr) - - # Convert C document to Ruby object - FFI::Bridge.document_from_ptr(doc_ptr) - end - end - - # Parse an XML file and return a Document - # - # This is a convenience method that reads the file and calls {.parse}. - # - # @param file_path [String] path to the XML file - # @return [Document] the parsed document - # @raise [Errno::ENOENT] if file does not exist - # @raise [ParseError] if XML is malformed - # - # @example - # doc = Taurus.parse_file('books.xml') - # doc.root.name # => root element name - # - # @see .parse - def parse_file(file_path) - xml_string = File.read(file_path) - parse(xml_string) - end - - # Evaluate an XPath expression on a document - # - # This is called internally by Document#xpath and Element#xpath. - # Uses the native C XPath evaluator via FFI. - # - # @param doc [Document] the document context - # @param expression [String] the XPath expression - # @param context_node [Element, Document] the context node for evaluation - # @param namespaces [Hash, nil] custom namespace mappings (reserved for future use) - # @return [Array, String, Float, Boolean] the XPath result - # - # @api private - def xpath_evaluate(doc, expression, context_node = nil, namespaces = nil) - # Note: namespaces parameter reserved for v0.9.0 custom namespace registration - # Currently, namespaces are auto-detected from the document (v0.8.0) - FFI::ErrorHandling.with_error_check do - # Extract C pointer from Ruby Document object or from context node - doc_ptr = doc.instance_variable_get(:@_c_ptr) - - # If doc doesn't have pointer, try to get it from context_node - # (happens when Element#xpath creates a temporary Document) - if !doc_ptr && context_node - doc_ptr = context_node.instance_variable_get(:@_c_doc_ptr) - end - - unless doc_ptr - raise Error, "Document has no C pointer (not created via Taurus.parse)" - end - - # Call C library to evaluate XPath - result_ptr = if context_node && context_node != doc - # Extract C pointer from context node - context_ptr = context_node.instance_variable_get(:@_c_ptr) - unless context_ptr - raise Error, "Context node has no C pointer" - end - - # Use context node variant - FFI.taurus_xpath_eval_with_context( - doc_ptr, - context_ptr, - expression, - expression.bytesize - ) - else - # Use document variant - FFI.taurus_xpath_eval(doc_ptr, expression, expression.bytesize) - end - - if result_ptr.null? - FFI::ErrorHandling.check_error! - raise Error, "XPath evaluation failed" - end - - # Wrap pointer for automatic cleanup - result_ptr = FFI::MemoryHelpers.wrap_xpath_result(result_ptr) - - # Convert C result to Ruby value, passing doc_ptr for full element hydration - FFI::Bridge.xpath_result_to_ruby(result_ptr, doc_ptr) - end - end - end + autoload :XML, "taurus/xml" end - -# Load adapter after Taurus module is fully defined -require_relative "taurus/adapter" \ No newline at end of file diff --git a/lib/taurus/.ruby_version b/lib/taurus/.ruby_version deleted file mode 100644 index 49ab6b8..0000000 --- a/lib/taurus/.ruby_version +++ /dev/null @@ -1 +0,0 @@ -3.1.1-arm64-darwin21 \ No newline at end of file diff --git a/lib/taurus/adapter.rb b/lib/taurus/adapter.rb deleted file mode 100644 index 06d3b09..0000000 --- a/lib/taurus/adapter.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# Load Moxml-compatible adapters if available -begin - require "moxml" - require_relative "adapter/taurus" -rescue LoadError - # Moxml not available, skip Moxml adapters -end diff --git a/lib/taurus/adapter/taurus.rb b/lib/taurus/adapter/taurus.rb deleted file mode 100644 index 4100d9e..0000000 --- a/lib/taurus/adapter/taurus.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -# This file is loaded from lib/taurus.rb after the main Taurus module is defined -# No need to require anything here - Taurus module is already available - -module Taurus - module Adapter - class Taurus < Moxml::Adapter::Base - class << self - def parse(xml, _options = {}) - ::Taurus.parse(xml) - rescue ::Taurus::ParseError => e - raise Moxml::ParseError.new( - e.message, - source: xml.is_a?(String) ? xml[0..100] : nil, - ) - end - - def xpath(node, expression, namespaces = {}) - # Taurus now has complete XPath 1.0 support through C extension - # Use the built-in xpath method - result = node.xpath(expression) - - # Ensure result is wrapped in NodeSet - case result - when Array - ::Taurus::NodeSet.new(result) - when ::Taurus::NodeSet - result - else - # Scalar values (string, number, boolean) - return as-is - result - end - end - - def at_xpath(node, expression, namespaces = {}) - result = xpath(node, expression, namespaces) - result.is_a?(::Taurus::NodeSet) ? result.first : result - end - - def xpath_supported? - true # Taurus has complete XPath 1.0 support - end - - def capabilities - { - # Core adapter capabilities - parse: true, - - # Parsing capabilities - sax_parsing: false, - namespace_aware: true, - namespace_support: :full, - dtd_support: false, - parsing_speed: :fast, - - # XPath capabilities - COMPLETE XPath 1.0 in C! - xpath_support: :full, - xpath_full: true, - xpath_axes: :complete, # All 13 XPath 1.0 axes - xpath_functions: :complete, # All 27 XPath 1.0 functions - xpath_predicates: true, - xpath_namespaces: true, - xpath_variables: true, - - # Serialization capabilities - namespace_serialization: true, - pretty_print: true, - - # Known limitations - schema_validation: false, - xslt_support: false, - } - end - end - end - - # For backward compatibility - TaurusAdapter = Taurus - end -end diff --git a/lib/taurus/adapters.rb b/lib/taurus/adapters.rb deleted file mode 100644 index aa51a37..0000000 --- a/lib/taurus/adapters.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -require_relative "adapter/taurus" -require_relative "adapter/headed_ox" - -module Taurus - # Adapter module for different XML processing backends - module Adapter - end -end \ No newline at end of file diff --git a/lib/taurus/attributes_hash.rb b/lib/taurus/attributes_hash.rb deleted file mode 100644 index e164f43..0000000 --- a/lib/taurus/attributes_hash.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -module Taurus - # A Hash subclass that allows transparent access with both string and symbol keys. - # - # AttributesHash is used to store XML element attributes, providing convenient - # access regardless of whether you use strings or symbols as keys. All keys are - # internally stored as symbols for consistency and performance. - # - # @example Access with symbols - # elem = doc.root - # elem.attributes[:id] # => "123" - # elem[:id] # => "123" (shorthand) - # - # @example Access with strings - # elem.attributes["id"] # => "123" (converted to symbol) - # elem["id"] # => "123" (shorthand) - # - # @example Setting attributes - # elem[:class] = "important" # stored as :class - # elem["data-id"] = "456" # stored as :"data-id" - # - # @example Best practice for performance - # # Prefer symbols for ~90% better performance - # elem[:id] # Fast path - # elem["id"] # Slower (needs conversion) - # - # @see Element#attributes - # @see Element#[] - # @see Element#[]= - class AttributesHash < Hash - # Retrieve a value by key, trying symbol and string variants - # - # Attempts to find the value using three strategies: - # 1. Direct lookup with the given key - # 2. Lookup with key converted to symbol - # 3. Lookup with key converted to string - # - # @param key [String, Symbol] the attribute name - # @return [String, nil] the attribute value or nil if not found - # - # @example - # attrs = AttributesHash.new - # attrs[:id] = "123" - # attrs[:id] # => "123" - # attrs["id"] # => "123" - # attrs[:other] # => nil - def [](key) - super(key) || super(key.to_sym) || super(key.to_s) - end - - # Set a value by key, always storing as a symbol - # - # All keys are normalized to symbols for consistent internal storage - # and optimal performance. - # - # @param key [String, Symbol] the attribute name - # @param value [String] the attribute value - # @return [String] the value that was set - # - # @example - # attrs = AttributesHash.new - # attrs["id"] = "123" # stored as :id - # attrs[:class] = "widget" # stored as :class - def []=(key, value) - super(key.to_sym, value) - end - - # Check if a key exists, trying symbol and string variants - # - # @param key [String, Symbol] the attribute name to check - # @return [Boolean] true if the key exists - # - # @example - # attrs = AttributesHash.new - # attrs[:id] = "123" - # attrs.key?(:id) # => true - # attrs.key?("id") # => true - # attrs.key?(:name) # => false - def key?(key) - super(key) || super(key.to_sym) || super(key.to_s) - end - - alias has_key? key? - end -end \ No newline at end of file diff --git a/lib/taurus/cli.rb b/lib/taurus/cli.rb deleted file mode 100644 index ad97807..0000000 --- a/lib/taurus/cli.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -require "thor" -require_relative "../taurus" -require_relative "commands/xpath_command" -require_relative "commands/format_command" - -module Taurus - # Taurus CLI - Command-line interface for XML processing - # - # Provides fast XML parsing, XPath queries, and formatting capabilities - # through a simple command-line interface. - # - # Architecture: - # - CLI layer uses Thor for option parsing - # - All logic delegated to Command classes - # - Commands use API classes (Document, Element, etc.) - # - MECE design: CLI/API/ENV argument handling - class CLI < Thor - # Global options available to all commands - class_option :quiet, - type: :boolean, - aliases: "-q", - desc: "Suppress output messages" - - class_option :verbose, - type: :boolean, - aliases: "-v", - desc: "Show verbose output" - - desc "xpath FILE EXPRESSION", "Execute XPath query against XML file" - long_desc <<~DESC - Execute an XPath 1.0 query against an XML document. - - Supports: - - All XPath 1.0 axes, functions, and operators - - Complete namespace support - - Multiple output formats - - Stdin input (use '-' as filename) - - Examples: - $ taurus xpath books.xml "//book" - $ cat books.xml | taurus xpath - "//title" - $ taurus xpath --count books.xml "//book[@price > 20]" - DESC - method_option :count, - type: :boolean, - aliases: "-c", - desc: "Output count of matching nodes" - method_option :boolean, - type: :boolean, - aliases: "-b", - desc: "Output boolean result (true/false)" - method_option :format, - type: :string, - aliases: "-f", - enum: %w[text xml count boolean], - default: "xml", - desc: "Output format" - def xpath(file, expression) - Commands::XPathCommand.new(options).run(file, expression) - rescue Taurus::Error => e - handle_error(e) - end - - desc "format FILE", "Pretty-print XML document" - long_desc <<~DESC - Format an XML document with proper indentation. - - Features: - - Customizable indentation (default: 2 spaces) - - Compact mode (removes extra whitespace) - - Output to file or stdout - - Preserves namespace declarations - - Examples: - $ taurus format books.xml - $ taurus format --indent 4 books.xml - $ taurus format --output formatted.xml books.xml - $ taurus format --compact books.xml - DESC - method_option :indent, - type: :numeric, - aliases: "-i", - default: 2, - desc: "Indentation spaces" - method_option :output, - type: :string, - aliases: "-o", - desc: "Output file (default: stdout)" - method_option :compact, - type: :boolean, - desc: "Remove extra whitespace" - def format(file) - Commands::FormatCommand.new(options).run(file) - rescue Taurus::Error => e - handle_error(e) - end - - desc "version", "Show Taurus version" - def version - puts "Taurus #{Taurus::VERSION}" - puts "Fast XML parser with complete XPath 1.0 support" - end - - private - - def handle_error(error) - if options[:verbose] - warn "Error: #{error.message}" - warn error.backtrace.join("\n") if error.backtrace - elsif !options[:quiet] - warn "Error: #{error.message}" - end - exit 1 - end - end -end \ No newline at end of file diff --git a/lib/taurus/commands/base.rb b/lib/taurus/commands/base.rb deleted file mode 100644 index e10fe91..0000000 --- a/lib/taurus/commands/base.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module Commands - # Base class for all Taurus commands - # - # Provides common functionality: - # - File/stdin reading - # - Error handling - # - Option access - # - Output management - class Base - attr_reader :options - - def initialize(options = {}) - @options = options - end - - # Read XML from file or stdin - # - # @param filename [String] File path or '-' for stdin - # @return [String] XML content - def read_input(filename) - if filename == "-" - $stdin.read - else - File.read(filename) - end - rescue Errno::ENOENT - raise Taurus::Error, "File not found: #{filename}" - rescue Errno::EACCES - raise Taurus::Error, "Permission denied: #{filename}" - rescue => e - raise Taurus::Error, "Failed to read file: #{e.message}" - end - - # Parse XML string - # - # @param xml [String] XML content - # @return [Taurus::Document] Parsed document - def parse_xml(xml) - Taurus.parse(xml) - rescue Taurus::ParseError => e - raise Taurus::Error, "XML parse error: #{e.message}" - end - - # Write output to file or stdout - # - # @param content [String] Content to write - # @param filename [String, nil] Output file or nil for stdout - def write_output(content, filename = nil) - if filename - File.write(filename, content) - log_info "Output written to #{filename}" if options[:verbose] - else - puts content - end - rescue => e - raise Taurus::Error, "Failed to write output: #{e.message}" - end - - # Log info message (unless quiet) - def log_info(message) - warn message unless options[:quiet] - end - - # Log verbose message (only if verbose) - def log_verbose(message) - warn message if options[:verbose] - end - end - end -end \ No newline at end of file diff --git a/lib/taurus/commands/format_command.rb b/lib/taurus/commands/format_command.rb deleted file mode 100644 index 984e320..0000000 --- a/lib/taurus/commands/format_command.rb +++ /dev/null @@ -1,187 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Taurus - module Commands - # Format command - Pretty-print XML documents - # - # Provides XML formatting with customizable indentation. - # Can output to stdout or file. - # - # Usage: - # command = FormatCommand.new(options) - # command.run("books.xml") - class FormatCommand < Base - # Format XML document - # - # @param filename [String] XML file path or '-' for stdin - def run(filename) - log_verbose "Reading XML from #{filename == '-' ? 'stdin' : filename}..." - xml = read_input(filename) - - log_verbose "Parsing XML..." - doc = parse_xml(xml) - - log_verbose "Formatting XML..." - formatted = format_document(doc) - - write_output(formatted, options[:output]) - end - - private - - # Format document based on options - def format_document(doc) - if options[:compact] - format_compact(doc) - else - format_pretty(doc) - end - end - - # Format with indentation - def format_pretty(doc) - indent = options[:indent] || 2 - indent_str = " " * indent - - xml = +"" # Unfreeze string - - # Add XML declaration if present - if doc.respond_to?(:version) && doc.version - xml << %{\n" - end - - # Format root element - xml << format_element(doc.root, 0, indent_str) - xml << "\n" - - xml - end - - # Format element with indentation - def format_element(element, depth, indent_str) - return +"" unless element - - indent = indent_str * depth - xml = +"#{indent}<#{element.name}" - - # Add attributes - if element.attributes && !element.attributes.empty? - element.attributes.each do |key, value| - xml << %( #{key}="#{escape_xml(value)}") - end - end - - # Handle empty elements - if element.nodes.empty? - xml << "/>" - return xml - end - - # Check if element has only text content - if has_only_text?(element) - xml << ">" - text = element.nodes.find { |n| n.is_a?(String) } - xml << escape_xml(text) if text - xml << "" - return xml - end - - xml << ">\n" - - # Format children - element.nodes.each do |child| - case child - when Element - xml << format_element(child, depth + 1, indent_str) - xml << "\n" - when String - # Only add text if non-whitespace - text = child.strip - unless text.empty? - xml << "#{indent}#{indent_str}#{escape_xml(text)}\n" - end - end - end - - xml << "#{indent}" - xml - end - - # Format document in compact mode (no extra whitespace) - def format_compact(doc) - xml = +"" - - # Add XML declaration if present - if doc.respond_to?(:version) && doc.version - xml << %{" - end - - # Format root element - xml << format_element_compact(doc.root) - xml - end - - # Format element in compact mode - def format_element_compact(element) - return +"" unless element - - xml = +"<#{element.name}" - - # Add attributes - if element.attributes && !element.attributes.empty? - element.attributes.each do |key, value| - xml << %( #{key}="#{escape_xml(value)}") - end - end - - # Handle empty elements - if element.nodes.empty? - xml << "/>" - return xml - end - - xml << ">" - - # Add children (remove whitespace-only text) - element.nodes.each do |child| - case child - when Element - xml << format_element_compact(child) - when String - # Only add non-whitespace text - text = child.strip - xml << escape_xml(text) unless text.empty? - end - end - - xml << "" - xml - end - - # Check if element contains only text content (no child elements) - def has_only_text?(element) - return false if element.nodes.empty? - - element.nodes.all? do |node| - node.is_a?(String) - end - end - - # Escape XML special characters - def escape_xml(text) - text.to_s - .gsub("&", "&") - .gsub("<", "<") - .gsub(">", ">") - .gsub('"', """) - .gsub("'", "'") - end - end - end -end \ No newline at end of file diff --git a/lib/taurus/commands/xpath_command.rb b/lib/taurus/commands/xpath_command.rb deleted file mode 100644 index 41c0be0..0000000 --- a/lib/taurus/commands/xpath_command.rb +++ /dev/null @@ -1,255 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Taurus - module Commands - # XPath query command - # - # Executes XPath 1.0 queries against XML documents. - # Supports all XPath axes, functions, and operators. - # - # Usage: - # command = XPathCommand.new(options) - # command.run("books.xml", "//book") - class XPathCommand < Base - # Execute XPath query - # - # @param filename [String] XML file path or '-' for stdin - # @param expression [String] XPath expression - def run(filename, expression) - log_verbose "Reading XML from #{filename == '-' ? 'stdin' : filename}..." - xml = read_input(filename) - - log_verbose "Parsing XML..." - doc = parse_xml(xml) - - log_verbose "Executing XPath: #{expression}" - result = doc.xpath(expression) - - log_verbose "Query returned #{result_summary(result)}" - - output_result(result) - end - - private - - # Output XPath result based on format and type - def output_result(result) - format = determine_format(result) - - case format - when :count - output_count(result) - when :boolean - output_boolean(result) - when :number - output_number(result) - when :string - output_string(result) - when :xml - output_xml(result) - end - end - - # Determine output format based on options and result type - def determine_format(result) - # Explicit format from options - return :count if options[:count] - return :boolean if options[:boolean] - return options[:format].to_sym if options[:format] - - # Auto-detect from result type - case result - when NodeSet, Array - :xml - when TrueClass, FalseClass - :boolean - when Numeric - :number - when String - :string - else - :xml - end - end - - # Output count of nodes - def output_count(result) - count = case result - when NodeSet, Array - result.size - when TrueClass - 1 - when FalseClass - 0 - else - 1 - end - puts count - end - - # Output boolean result - def output_boolean(result) - bool = case result - when NodeSet, Array - !result.empty? - when TrueClass, FalseClass - result - when Numeric - result != 0 - when String - !result.empty? - else - false - end - puts bool ? "true" : "false" - end - - # Output number result - def output_number(result) - number = case result - when Numeric - result - when String - result.to_f - when TrueClass - 1.0 - when FalseClass - 0.0 - else - 0.0 - end - - # Handle special float values - if number.infinite? - puts number > 0 ? "Infinity" : "-Infinity" - elsif number.nan? - puts "NaN" - else - puts number - end - end - - # Output string result - def output_string(result) - str = case result - when String - result - when Numeric - result.to_s - when TrueClass - "true" - when FalseClass - "false" - when NodeSet, Array - result.map { |n| node_to_string(n) }.join - else - result.to_s - end - puts str - end - - # Output XML nodes - def output_xml(result) - nodes = case result - when NodeSet - result.to_a - when Array - result - when Element - [result] - else - return output_string(result) - end - - if nodes.empty? - log_info "XPath set is empty" unless options[:quiet] - exit 11 # XMLLINT_ERR_XPATH_EMPTY - end - - nodes.each do |node| - puts node_to_xml(node) - end - end - - # Convert node to XML string - def node_to_xml(node) - case node - when Element - element_to_xml(node) - when String - node - else - node.to_s - end - end - - # Convert element to XML string - def element_to_xml(element) - xml = +"<#{element.name}" - - # Add attributes - if element.attributes && !element.attributes.empty? - element.attributes.each do |key, value| - xml << %( #{key}="#{escape_xml(value)}") - end - end - - # Handle empty elements - if element.nodes.empty? - xml << "/>" - return xml - end - - xml << ">" - - # Add children - element.nodes.each do |child| - xml << node_to_xml(child) - end - - xml << "" - xml - end - - # Convert node to text string (for string() function) - def node_to_string(node) - case node - when Element - element.nodes.map { |n| node_to_string(n) }.join - when String - node - else - node.to_s - end - end - - # Escape XML special characters - def escape_xml(text) - text.to_s - .gsub("&", "&") - .gsub("<", "<") - .gsub(">", ">") - .gsub('"', """) - .gsub("'", "'") - end - - # Human-readable result summary - def result_summary(result) - case result - when NodeSet, Array - "#{result.size} node(s)" - when TrueClass, FalseClass - "boolean: #{result}" - when Numeric - "number: #{result}" - when String - "string: #{result[0...50]}#{'...' if result.length > 50}" - else - result.class.name - end - end - end - end -end \ No newline at end of file diff --git a/lib/taurus/document.rb b/lib/taurus/document.rb deleted file mode 100644 index 8d2cd43..0000000 --- a/lib/taurus/document.rb +++ /dev/null @@ -1,155 +0,0 @@ -# frozen_string_literal: true - -require_relative "element" - -module Taurus - # Represents an XML document with optional XML declaration (prolog). - # - # Document is the top-level container for an XML tree. It extends {Element} - # and adds support for XML version, encoding, and standalone attributes. - # The document maintains a root element and provides optimized access to it. - # - # @example Parse a document - # doc = Taurus.parse('') - # doc.version # => "1.0" - # doc.root.name # => "root" - # - # @example Create a document programmatically - # doc = Taurus::Document.new(version: "1.0", encoding: "UTF-8") - # root = Taurus::Element.new("root") - # doc.root = root - # - # @example XPath queries on document - # doc = Taurus.parse('Ruby') - # titles = doc.xpath('//title') - # count = doc.xpath('count(//book)') # => 1.0 - # - # @see Element - # @see #root - # @see #xpath - class Document < Element - # @return [String] the XML version (default: "1.0") - # @return [String, nil] the character encoding (e.g., "UTF-8") - # @return [Boolean, nil] whether the document is standalone - attr_accessor :version, :encoding, :standalone - - # Create a new XML document - # - # @param prolog [Hash] XML declaration attributes - # @option prolog [String] :version ("1.0") the XML version - # @option prolog [String] :encoding the character encoding - # @option prolog [Boolean] :standalone whether document is standalone - # - # @example Default document - # doc = Taurus::Document.new - # doc.version # => "1.0" - # - # @example Document with encoding - # doc = Taurus::Document.new(encoding: "UTF-8") - def initialize(prolog = {}) - super(nil) - @version = prolog[:version] || "1.0" - @encoding = prolog[:encoding] - @standalone = prolog[:standalone] - @nodes = [] - end - - # Returns the root element of the document - # - # This method uses a two-level optimization strategy: - # 1. Ruby cache (@root) - set on first access or assignment - # 2. C fast-path (root_fast) - blazing fast ivar access from C - # - # The first call scans the nodes array to find the root element. - # Subsequent calls use the cached value for O(1) access. - # - # @return [Element, nil] the root element or nil if no root exists - # - # @example - # doc = Taurus.parse('') - # root = doc.root - # root.name # => "root" - # - # @see #root= - def root - # If cache exists, return cached value - return @root if instance_variable_defined?(:@root) - - # First access - find and cache root - @root = nodes.find { |n| n.is_a?(Element) } - end - - # Sets the root element of the document - # - # Replaces any existing root element with the given element. - # The root element becomes a child node of the document. - # - # @param element [Element, nil] the new root element or nil to remove root - # @return [Element, nil] the assigned element - # - # @example - # doc = Taurus::Document.new - # root = Taurus::Element.new("root") - # doc.root = root - # doc.root.name # => "root" - # - # @see #root - def root=(element) - # Remove existing root if present - nodes.delete_if { |n| n.is_a?(Element) } - # Add new root - nodes << element if element - # Update cache - @root = element - end - - # Execute an XPath query on the document - # - # Evaluates the given XPath expression with the document as both the - # document context and the context node. Supports full XPath 1.0 specification - # including all 13 axes, 27 functions, and operators. - # - # @param expression [String] the XPath expression to evaluate - # @param namespaces [Hash{String => String}, nil] optional custom namespace mappings - # (prefix => URI). Overrides auto-detected namespaces. - # @return [Array, String, Numeric, Boolean] the query result - # - Node-set queries return Array - # - String queries return String - # - Numeric queries return Float - # - Boolean queries return true/false - # - # @raise [ArgumentError] if expression is invalid - # - # @example Find all elements - # doc.xpath('//book') # => [, , ...] - # - # @example Count elements - # doc.xpath('count(//book)') # => 2.0 - # - # @example Boolean query - # doc.xpath('boolean(//book[@price > 20])') # => true - # - # @example String query - # doc.xpath('string(//title)') # => "First Title" - # - # @example Custom namespaces - # doc.xpath('//ns:book', namespaces: { 'ns' => 'http://books.org' }) - # - # @see Element#xpath - # @see XPath - def xpath(expression, namespaces: nil) - # Validate XPath expression - if expression.nil? || expression.empty? - raise Taurus::ParseError.new( - "Empty XPath expression", - code: :empty_input, - line: 0, - column: 0 - ) - end - - # Use root element as context node (Document itself is the document context) - Taurus.xpath_evaluate(self, expression, root, namespaces) - end - end -end \ No newline at end of file diff --git a/lib/taurus/element.rb b/lib/taurus/element.rb deleted file mode 100644 index 2589ec1..0000000 --- a/lib/taurus/element.rb +++ /dev/null @@ -1,762 +0,0 @@ -# frozen_string_literal: true - -require_relative "node" -require_relative "attributes_hash" - -module Taurus - # Represents an XML element with full namespace support and Ox-compatible API. - # - # Element is the core class for representing XML elements in the document tree. - # It provides comprehensive functionality for: - # - Element and attribute access - # - Namespace handling with full XML Namespaces 1.0 support - # - XPath querying - # - DOM manipulation (add/remove children) - # - Ox-compatible API for easy migration - # - # @example Create and manipulate elements - # elem = Taurus::Element.new("book") - # elem[:id] = "123" - # elem << Taurus::Element.new("title") - # elem.name # => "book" - # elem[:id] # => "123" - # - # @example Parse and query - # doc = Taurus.parse('Ruby') - # book = doc.root.nodes.first - # book.name # => "book" - # book[:id] # => "1" - # book.text # => "Ruby" (first text node) - # - # @example Namespace support - # xml = '' - # doc = Taurus.parse(xml) - # doc.root.namespace[:href] # => "http://example.org" - # doc.root.namespace_for_prefix(nil) # => "http://example.org" - # - # @example XPath queries - # books = doc.root.xpath('.//book[@price > 20]') - # count = doc.root.xpath('count(.//book)') # => 2.0 - # - # @see Node - # @see Document - # @see AttributesHash - class Element < Node - # @return [AttributesHash] the element's attributes with dual string/symbol access - attr_reader :attributes - - # Create a new element with the given name - # - # @param name [String, Symbol] the element name (tag name) - # - # @example - # book = Taurus::Element.new("book") - # book.name # => "book" - def initialize(name) - super(name) - @attributes = AttributesHash.new - @nodes = [] - end - - # Return the element's name (tag name) - # - # Provides direct access to the element name for maximum performance. - # Names are automatically interned and frozen in the C parser for memory - # efficiency and VM optimization. - # - # @return [String] the element name - # - # @example - # elem = Taurus::Element.new("book") - # elem.name # => "book" - # - # @note This is optimized for performance with direct ivar access - def name - @value - end - - # Get the fully qualified name with namespace prefix if present - # - # @return [String] the qualified name (e.g., "prefix:localname" or "localname") - # - # @example Without prefix - # elem.qualified_name # => "book" - # - # @example With prefix - # elem.qualified_name # => "ex:book" - # - # @see #namespace_prefix - def qualified_name - if namespace_prefix - "#{namespace_prefix}:#{value}" - else - value - end - end - - alias name= value= - - # Returns the element's child nodes - # - # The nodes array contains all children including element nodes and text nodes. - # This array is always initialized and ready for direct access, providing - # excellent performance (matches or exceeds Ox). - # - # @return [Array] array of child nodes - # - # @example Access children - # elem.nodes # => [, "text", ] - # elem.nodes.first # => - # - # @example Iterate children - # elem.nodes.each { |node| puts node } - # - # @note This is performance-optimized with direct ivar access - def nodes - @nodes - end - - # Append a child node to this element - # - # Adds the node to the end of the children array. For Element nodes, - # also sets the parent relationship. - # - # @param node [Element, String] the node to append - # @return [Element] self for method chaining - # @raise [ArgumentError] if node is not a String or Node - # - # @example Append element - # parent = Taurus::Element.new("parent") - # child = Taurus::Element.new("child") - # parent << child - # parent.nodes # => [] - # - # @example Append text - # parent << "text content" - # parent.text # => "text content" - # - # @example Method chaining - # parent << child1 << child2 << "text" - # - # @see #add_child - # @see #prepend_child - def <<(node) - raise ArgumentError, "argument to << must be a String or Taurus::Node" unless node.is_a?(String) || node.is_a?(Node) - - @nodes << node - node.parent = self if node.is_a?(Element) # Set parent for Element nodes - self - end - - # Prepend a child node to the beginning of the children array - # - # @param node [Element, String] the node to prepend - # @return [Element] self for method chaining - # @raise [ArgumentError] if node is not a String or Node - # - # @example - # parent = Taurus::Element.new("parent") - # parent << "second" - # parent.prepend_child("first") - # parent.nodes # => ["first", "second"] - # - # @see #<< - # @see #add_child - def prepend_child(node) - raise ArgumentError, "argument to prepend_child must be a String or Taurus::Node" unless node.is_a?(String) || node.is_a?(Node) - - @nodes.unshift(node) - self - end - - # Check equality with another element - # - # Two elements are equal if they have the same name, attributes, and children. - # This provides deep equality checking. - # - # @param other [Element] the element to compare with - # @return [Boolean] true if elements are equal - # - # @example - # elem1 = Taurus::Element.new("book") - # elem1[:id] = "1" - # elem2 = Taurus::Element.new("book") - # elem2[:id] = "1" - # elem1 == elem2 # => true (if same children) - def eql?(other) - return false unless super - return false unless attributes == other.attributes - return false unless nodes == other.nodes - true - end - alias == eql? - - # Returns the first text node content - # - # Searches through child nodes and returns the content of the first text node. - # Returns nil if no text nodes exist. - # - # @return [String, nil] the text content or nil - # - # @example - # elem = Taurus::Element.new("title") - # elem << "Ruby Programming" - # elem.text # => "Ruby Programming" - # - # @example Multiple children - # elem << "First" << Taurus::Element.new("tag") << "Second" - # elem.text # => "First" (returns first text node only) - # - # @see #replace_text - def text - nodes.each { |n| return n if n.is_a?(String) } - nil - end - - # Replace all child nodes with a single text node - # - # Clears the children array and replaces it with a single text node. - # Useful for setting simple text content. - # - # @param txt [String] the new text content - # @return [Array] the new nodes array containing just the text - # @raise [ArgumentError] if txt is not a String - # - # @example - # elem = Taurus::Element.new("title") - # elem << "Old" << Taurus::Element.new("tag") - # elem.replace_text("New") - # elem.nodes # => ["New"] - # elem.text # => "New" - # - # @see #text - def replace_text(txt) - raise ArgumentError, "argument to replace_text() must be a String" unless txt.is_a?(String) - - @nodes = [txt] - end - - # Set or get an attribute value - # ✅ PERFORMANCE: Symbol fast-path avoids conversion overhead - # Old: Always convert to symbol, then check string fallbacks (71.49% waste) - # New: Direct lookup for symbols (90% of usage), convert only for strings - # Expected: 1.5× speedup (0.11µs → 0.07µs) - def [](attr) - # Fast path: direct symbol lookup (90% of real-world usage) - return @attributes[attr] if attr.is_a?(Symbol) - - # Slow path: string conversion for backwards compatibility (10% of usage) - key = attr.to_sym - @attributes[key] || @attributes[attr.to_s] || @attributes[attr] - end - - def []=(attr, value) - key = attr.is_a?(Symbol) ? attr : attr.to_sym - @attributes[key] = value - end - - # Remove this element from its parent - def remove - return self if parent.nil? - - parent.nodes.delete(self) if parent.respond_to?(:nodes) - self.parent = nil - self - end - - # Add a child element to this element - def add_child(child) - child.remove if child.respond_to?(:parent) && child.parent - nodes << child - child.parent = self if child.respond_to?(:parent=) - self - end - - # Create an element with a namespace declaration - # - # This is a class method for convenient creation of namespaced elements. - # - # @param name [String] the element name - # @param prefix [String, nil] the namespace prefix (nil for default namespace) - # @param href [String] the namespace URI - # @return [Element] the new element with namespace - # - # @example Default namespace - # elem = Taurus::Element.with_namespace("root", href: "http://example.org") - # elem.namespace[:href] # => "http://example.org" - # - # @example Prefixed namespace - # elem = Taurus::Element.with_namespace("root", - # prefix: "ex", - # href: "http://example.org") - # elem.namespace_prefix # => "ex" - # - # @see #add_namespace - def self.with_namespace(name, prefix: nil, href:) - elem = new(name) - if prefix - elem.attributes["xmlns:#{prefix}"] = href - elem.add_namespace(prefix, href) - else - elem.attributes["xmlns"] = href - elem.add_namespace(nil, href) - end - elem - end - - # Set the parent element with validation - # - # Sets the parent reference, ensuring no circular references or invalid parents. - # Automatically removes the element from its old parent's children array. - # - # @param new_parent [Element, nil] the new parent element - # @return [Element, nil] the new parent - # @raise [ArgumentError] if trying to set element as its own parent - # @raise [TypeError] if new_parent is not an Element or nil - # - # @example - # child = Taurus::Element.new("child") - # parent = Taurus::Element.new("parent") - # child.parent = parent - # child.parent # => - # - # @note Circular references are silently ignored for safety - def parent=(new_parent) - if new_parent == self - raise ArgumentError, "Cannot set element as its own parent" - end - - unless new_parent.nil? || new_parent.is_a?(Element) - raise TypeError, "Parent must be an Element or nil" - end - - # Check for circular reference by walking up the parent chain - current = new_parent - while current - if current == self - return # Ignore circular reference - end - current = current.parent - end - - # Remove from old parent's nodes if any - if @parent && @parent.respond_to?(:nodes) - @parent.nodes.delete(self) - end - - @parent = new_parent - end - - # @!group Namespace Methods - - # Get the primary namespace of this element - # - # Returns the namespace that applies to this element, which may be a local - # namespace declaration or inherited from an ancestor. - # - # @return [Hash{Symbol => String}, nil] namespace hash with :prefix and :href keys, or nil - # - # @example - # xml = '' - # doc = Taurus.parse(xml) - # doc.root.namespace # => {prefix: nil, href: "http://example.org"} - # - # @see #namespaces - # @see #namespace_for_prefix - def namespace - return nil unless @_namespace_data - - @_namespace_data - end - - # Get all namespace declarations on this element only - # - # Returns only the namespace declarations directly on this element, - # not inherited ones. - # - # @return [Array] array of namespace hashes with :prefix and :href keys - # - # @example - # xml = '' - # doc = Taurus.parse(xml) - # doc.root.namespaces - # # => [{prefix: nil, href: "http://default.org"}, - # # {prefix: "ex", href: "http://example.org"}] - # - # @see #namespace - # @see #all_namespaces - def namespaces - @_namespaces ||= [] - end - - # Resolve a namespace prefix to its URI, checking ancestors - # - # Searches for the namespace URI associated with the given prefix, - # checking local declarations first, then walking up the parent chain. - # This implements proper XML Namespaces 1.0 inheritance. - # - # @param prefix [String, nil] the namespace prefix (nil for default namespace) - # @return [String, nil] the namespace URI or nil if not found - # - # @example Default namespace - # xml = '' - # doc = Taurus.parse(xml) - # doc.root.nodes.first.namespace_for_prefix(nil) - # # => "http://example.org" (inherited) - # - # @example Prefixed namespace - # elem.namespace_for_prefix("ex") # => "http://example.org" - # - # @see #namespace - # @see #add_namespace - def namespace_for_prefix(prefix) - # Check local namespaces first - namespaces.each do |ns| - if prefix.nil? - return ns[:href] if ns[:prefix].nil? - elsif ns[:prefix] == prefix - return ns[:href] - end - end - - # Check parent namespaces - parent&.namespace_for_prefix(prefix) - end - - # Add a namespace declaration to this element - # - # Declares a new namespace on this element, adding both to the internal - # namespace list and to the element's attributes. - # - # @param prefix [String, nil] the namespace prefix (nil for default namespace) - # @param href [String] the namespace URI - # @return [void] - # - # @example Add default namespace - # elem.add_namespace(nil, "http://example.org") - # elem.attributes["xmlns"] # => "http://example.org" - # - # @example Add prefixed namespace - # elem.add_namespace("ex", "http://example.org") - # elem.attributes["xmlns:ex"] # => "http://example.org" - # - # @see .with_namespace - def add_namespace(prefix, href) - @_namespaces ||= [] - @_namespaces << { prefix: prefix, href: href } - - # Update attributes to include xmlns declaration - if prefix.nil? - @attributes["xmlns"] = href - else - @attributes["xmlns:#{prefix}"] = href - end - end - - # Get the namespace prefix for this element - # - # @return [String, nil] the prefix or nil for default namespace - # - # @example - # elem.namespace_prefix # => "ex" - # - # @see #namespace_uri - def namespace_prefix - namespace&.[](:prefix) - end - - # Get the namespace URI for this element - # - # @return [String, nil] the namespace URI or nil - # - # @example - # elem.namespace_uri # => "http://example.org" - # - # @see #namespace_prefix - def namespace_uri - namespace&.[](:href) - end - - # Check if element has a namespace - # - # @return [Boolean] true if element has a namespace - # - # @example - # elem.namespace? # => true - def namespace? - !namespace.nil? - end - - # Get all namespace definitions including inherited ones - # - # Returns a hash of all namespaces in scope for this element, - # including both local declarations and inherited ones from ancestors. - # Local declarations override inherited ones. - # - # @return [Hash{String => String}] map of prefixes (or nil) to URIs - # - # @example - # xml = '' - # doc = Taurus.parse(xml) - # item = doc.root.nodes.first - # item.all_namespaces - # # => {nil => "http://default.org", "ex" => "http://example.org"} - # - # @see #namespaces - # @see #namespace_for_prefix - def all_namespaces - result = {} - - # Add inherited namespaces from parents - if parent - parent.all_namespaces.each do |prefix, href| - result[prefix] = href - end - end - - # Override with local declarations - namespaces.each do |ns| - prefix = ns[:prefix] - result[prefix] = ns[:href] - end - - result - end - - # @!endgroup - - # Locate nodes by path (Ox-compatible) - # - # Provides Ox-compatible path-based node location using a simplified - # path syntax. For XPath queries, use {#xpath} instead. - # - # @param path [String, nil] the path pattern to match - # @return [Array] matching elements - # - # @example Simple path - # elem.locate('child/grandchild') - # - # @example Wildcard - # elem.locate('*/item') - # - # @note For full XPath support, use {#xpath} instead - # @see #xpath - def locate(path) - return [self] if path.nil? - - found = [] - pa = path.split('/') - if path.start_with?('*') - # Allow self to be checked - e = Element.new('') - e << self - e.alocate(pa, found) - else - alocate(pa, found) - end - found - end - - # Dynamic method dispatch for child element access - # - # Allows accessing child elements and attributes using method syntax. - # This provides a convenient alternative to navigating the nodes array. - # - # @param id [Symbol] the method name (element name or attribute key) - # @param args [Array] optional index for multiple matching elements - # @return [Element, String, nil] the matched element, attribute value, or nil - # @raise [NoMethodError] if no matching element or attribute found - # - # @example Access child element - # parent.child_name # => element - # - # @example Access multiple elements by index - # parent.item(0) # => first - # parent.item(1) # => second - # - # @example Access attribute - # elem.id # => attribute value if no child named "id" - # - # @note This method only works when no regular method conflicts - def method_missing(id, *args, &block) - has_some = false - ids = id.to_s - i = args[0].to_i - - nodes.each do |n| - next unless (n.is_a?(Element) || n.is_a?(Instruct)) && (n.value == id || n.value == ids) - return n if i == 0 - - has_some = true - i -= 1 - end - - return @attributes[id] if @attributes.key?(id) - return @attributes[ids] if @attributes.key?(ids) - return nil if has_some - - raise NoMethodError, "#{ids} not found" - end - - # Check if method_missing will handle the given method - # - # @param id [Symbol] the method name to check - # @param inc_all [Boolean] whether to include all methods - # @return [Boolean] true if the method will be handled - # - # @api private - def respond_to_missing?(id, inc_all = false) - id_str = id.to_s - id_sym = id.to_sym - - nodes.each do |n| - next if n.is_a?(String) - return true if n.respond_to?(:value) && (n.value == id_str || n.value == id_sym) - end - - return true if @attributes.key?(id_str) || @attributes.key?(id_sym) - - false - end - - # Execute an XPath query on this element - # - # Evaluates the XPath expression with this element as the context node. - # Automatically finds or creates the document context for proper evaluation. - # Supports full XPath 1.0 specification. - # - # @param expression [String] the XPath expression to evaluate - # @param namespaces [Hash{String => String}, nil] optional custom namespace mappings - # (prefix => URI). Overrides auto-detected namespaces. - # @return [Array, String, Float, Boolean] the query result - # - Node-set queries return Array - # - String queries return String - # - Numeric queries return Float - # - Boolean queries return true/false - # - # @example Find descendants - # elem.xpath('.//book') # => [, , ...] - # - # @example Count descendants - # elem.xpath('count(.//book)') # => 2.0 - # - # @example Use predicates - # elem.xpath('.//book[@price > 20]') - # - # @example Relative paths - # elem.xpath('./child | ./other') # Union - # - # @example Custom namespaces - # elem.xpath('//ns:book', namespaces: { 'ns' => 'http://books.org' }) - # - # @see Document#xpath - # @see XPath - def xpath(expression, namespaces: nil) - # Validate XPath expression - raise RuntimeError, "XPath parsing error: empty expression" if expression.nil? || expression.empty? - - # Find the document root for context - current = self - current = current.parent while current.parent && !current.is_a?(Document) - - # If we found a Document, use it - if current.is_a?(Document) - doc = current - else - # No Document found - current is the root element - # If this element has C pointers, create a document that preserves them - if instance_variable_defined?(:@_c_doc_ptr) && @_c_doc_ptr - # Element was parsed from C - create Document wrapper with C pointer - doc = Document.new - doc.instance_variable_set(:@_c_ptr, @_c_doc_ptr) - doc.root = current - else - # Pure Ruby element - create temporary document - doc = Document.new - doc.root = current - end - end - - # Call the C extension's xpath_evaluate function - Taurus.xpath_evaluate(doc, expression, self, namespaces) - end - - protected - - # Internal locate implementation - def alocate(path, found) - step = path[0] - - if step.start_with?('@') # attribute - raise Taurus::Error, "Invalid path" unless path.size == 1 - - step = step[1..-1] - sym_step = step.to_sym - @attributes.each do |k, v| - found << v if step == '?' || k == step || k == sym_step - end - else # element name - if (i = step.index('[')).nil? - name = step - qual = nil - else - name = step[0..i-1] - raise Taurus::Error, "Invalid path" unless step.end_with?(']') - - i += 1 - qual = step[i..i] - qual = '+' if qual.between?('0', '9') - i += 1 unless qual == '+' - index = step[i..-2].to_i - end - - # Select matching nodes - match = if ['?', '*'].include?(name) - nodes - elsif name.start_with?('^') - class_name = name[1..-1] - nodes.select do |e| - case class_name - when 'Element' then e.is_a?(Element) - when 'String', 'Text' then e.is_a?(String) - when 'Comment' then e.is_a?(Comment) rescue false - when 'CData' then e.is_a?(CData) rescue false - else false - end - end - else - nodes.select { |e| e.is_a?(Element) && name == e.name } - end - - # Apply qualifiers - unless qual.nil? || match.empty? - match = case qual - when '+' then index < match.size ? [match[index]] : [] - when '-' then index <= match.size ? [match[-index]] : [] - when '<' then index > 0 ? match[0..index-1] : [] - when '>' then index <= match.size ? match[index+1..-1] : [] - when '@' - k, v = step[i..-2].split('=') - if v - match.select { |n| n.is_a?(Element) && (v == n.attributes[k.to_sym] || v == n.attributes[k]) } - else - match.select { |n| n.is_a?(Element) && (n.attributes[k.to_sym] || n.attributes[k]) } - end - else - raise Taurus::Error, "Invalid path" - end - end - - # Recurve or add to found - if path.size == 1 - match.each { |n| found << n } - elsif name == '*' - match.each { |n| n.alocate(path, found) if n.is_a?(Element) } - match.each { |n| n.alocate(path[1..-1], found) if n.is_a?(Element) } - else - match.each { |n| n.alocate(path[1..-1], found) if n.is_a?(Element) } - end - end - end - end -end \ No newline at end of file diff --git a/lib/taurus/ffi/bridge.rb b/lib/taurus/ffi/bridge.rb deleted file mode 100644 index 10cd724..0000000 --- a/lib/taurus/ffi/bridge.rb +++ /dev/null @@ -1,276 +0,0 @@ -# frozen_string_literal: true - -require_relative 'library' -require_relative 'types' -require_relative 'memory' -require_relative 'errors' - -module Taurus - module FFI - # Bridge between C pointers and Ruby objects - # Converts taurus_document/taurus_element pointers to Ruby Document/Element instances - module Bridge - class << self - # Convert a taurus_document pointer to a Ruby Document object - # @param doc_ptr [FFI::Pointer] the document pointer from C (wrapped in AutoPointer) - # @return [Document] the Ruby Document object - def document_from_ptr(doc_ptr) - return nil if doc_ptr.nil? || doc_ptr.null? - - # Get encoding from C - encoding_str = Taurus::FFI.taurus_document_encoding(doc_ptr) - - # Create Ruby Document with prolog - doc = Document.new( - version: "1.0", - encoding: encoding_str - ) - - # CRITICAL: Store C pointer for XPath evaluation - # The AutoPointer will handle cleanup when Ruby object is GC'd - doc.instance_variable_set(:@_c_ptr, doc_ptr) - - # Get root element - root_ptr = Taurus::FFI.taurus_document_root(doc_ptr) - if root_ptr && !root_ptr.null? - root_elem = element_from_ptr(root_ptr, doc_ptr) - doc.root = root_elem if root_elem - end - - doc - end - - # Convert a taurus_element pointer to a Ruby Element object - # @param elem_ptr [FFI::Pointer] the element pointer from C - # @param doc_ptr [FFI::Pointer] the document pointer (for XPath context, optional) - # @return [Element] the Ruby Element object - def element_from_ptr(elem_ptr, doc_ptr = nil) - return nil if elem_ptr.nil? || elem_ptr.null? - - # Safety check: detect attribute nodes before calling C functions - # Attribute nodes have node_type (int, value 1) as first 4 bytes - # Element nodes have name pointer (8 bytes) as first field - begin - first_bytes = elem_ptr.read_bytes(4) - first_int = first_bytes.unpack1('L') # Little-endian uint32 - rescue => e - # If we can't read the pointer safely, it's invalid - warn "Warning: Cannot read pointer in element_from_ptr: #{e.message}" if $DEBUG - return nil - end - - # If first 4 bytes == 1, it's TAURUS_NODE_ATTRIBUTE - return nil if first_int == 1 - - # Get element name and freeze it (string interning optimization) - begin - name = Taurus::FFI.taurus_element_name(elem_ptr) - return nil unless name - name = name.freeze - rescue => e - # Pointer is not a valid element - warn "Warning: Invalid element pointer in element_from_ptr: #{e.message}" if $DEBUG - return nil - end - - # Create Ruby Element - elem = Element.new(name) - - # CRITICAL: Store C pointers for XPath evaluation - elem.instance_variable_set(:@_c_ptr, elem_ptr) - elem.instance_variable_set(:@_c_doc_ptr, doc_ptr) if doc_ptr - - # Get text content (for text nodes) - text = Taurus::FFI.taurus_element_text(elem_ptr) - if text && !text.empty? - elem.instance_variable_set(:@_text_content, text) - end - - # Set namespace data if present - ns_uri = Taurus::FFI.taurus_element_namespace(elem_ptr) - ns_prefix = Taurus::FFI.taurus_element_prefix(elem_ptr) - - if ns_uri && !ns_uri.empty? - elem.instance_variable_set(:@_namespace_data, { - prefix: (ns_prefix && !ns_prefix.empty?) ? ns_prefix : nil, - href: ns_uri - }) - end - - # Load attributes - load_attributes(elem_ptr, elem) - - # Load namespace declarations - load_namespaces(elem_ptr, elem) - - # Load children - load_children(elem_ptr, elem, doc_ptr) - - elem - end - - # Load attributes from C element to Ruby element - # @param elem_ptr [FFI::Pointer] the C element pointer - # @param elem [Element] the Ruby element - # @return [void] - def load_attributes(elem_ptr, elem) - count = Taurus::FFI.taurus_element_attribute_count(elem_ptr) - - count.times do |i| - begin - attr_ptr = Taurus::FFI.taurus_element_attribute(elem_ptr, i) - next if attr_ptr.nil? || attr_ptr.null? - - name = Taurus::FFI.taurus_attribute_name(attr_ptr) - value = Taurus::FFI.taurus_attribute_value(attr_ptr) - - elem.attributes[name.to_sym] = value if name && value - rescue => e - # Skip invalid attribute pointers - warn "Warning: Failed to load attribute #{i}: #{e.message}" if $DEBUG - next - end - end - end - - # Load namespace declarations from C element to Ruby element - # @param elem_ptr [FFI::Pointer] the C element pointer - # @param elem [Element] the Ruby element - # @return [void] - def load_namespaces(elem_ptr, elem) - count = Taurus::FFI.taurus_element_namespace_count(elem_ptr) - - count.times do |i| - ns_ptr = Taurus::FFI.taurus_element_namespace_decl(elem_ptr, i) - next if ns_ptr.null? - - prefix = Taurus::FFI.taurus_namespace_prefix(ns_ptr) - uri = Taurus::FFI.taurus_namespace_uri(ns_ptr) - - if uri && !uri.empty? - prefix = nil if prefix && prefix.empty? - elem.add_namespace(prefix, uri) - end - end - end - - # Load children from C element to Ruby element - # @param elem_ptr [FFI::Pointer] the C element pointer - # @param elem [Element] the Ruby element - # @param doc_ptr [FFI::Pointer] the document pointer (optional) - # @return [void] - def load_children(elem_ptr, elem, doc_ptr = nil) - count = Taurus::FFI.taurus_element_child_count(elem_ptr) - - # If we have text content, add it as a text node - if elem.instance_variable_get(:@_text_content) - elem << elem.instance_variable_get(:@_text_content) - end - - count.times do |i| - child_ptr = Taurus::FFI.taurus_element_child(elem_ptr, i) - next if child_ptr.null? - - # Recursively convert child element - child = element_from_ptr(child_ptr, doc_ptr) - elem << child if child - end - end - - # Convert XPath result to Ruby value - # @param result_ptr [FFI::Pointer] the XPath result pointer - # @param doc_ptr [FFI::Pointer] the document pointer (for full element hydration) - # @return [Array, String, Float, Boolean] the converted result - def xpath_result_to_ruby(result_ptr, doc_ptr = nil) - return nil if result_ptr.null? - - # Get result type - type_int = Taurus::FFI.taurus_xpath_result_get_type(result_ptr) - type = Taurus::FFI.xpath_result_type_to_sym(type_int) - - case type - when :boolean - bool_int = Taurus::FFI.taurus_xpath_result_as_boolean(result_ptr) - bool_int != 0 - - when :number - Taurus::FFI.taurus_xpath_result_as_number(result_ptr) - - when :string - str = Taurus::FFI.taurus_xpath_result_as_string(result_ptr) - # v1.1.0: FFI returns ASCII-8BIT by default, force UTF-8 encoding - # The C library produces correct UTF-8 bytes, we just need to mark them correctly - str.force_encoding(Encoding::UTF_8) if str - str - - when :nodeset - # Convert nodeset to Ruby array by reading XPathNodeSet struct directly - # This bypasses the public API which filters out attributes - - # struct taurus_xpath_result { type(4+4pad), value(union at offset 8) } - # value.nodeset_value is XPathNodeSet* - nodeset_ptr = result_ptr.get_pointer(8) - return [] if nodeset_ptr.null? - - # struct xpath_nodeset { void** nodes, size_t count, size_t capacity } - nodes_array_ptr = nodeset_ptr.read_pointer # void** nodes at offset 0 - count = nodeset_ptr.get_uint64(8) # size_t count at offset 8 (8 bytes on 64-bit) - - result = [] - count.times do |i| - # Read void* from nodes array - node_ptr = nodes_array_ptr.get_pointer(i * ::FFI::Pointer.size) - next if node_ptr.null? - - # Type detection: Only attribute nodes have a node_type field! - # Elements have char* name as first field (8 bytes on 64-bit). - # Read first 4 bytes and check if it's the ATTRIBUTE enum value (1). - # If not 1, treat as element (no type field exists). - first_int = node_ptr.read_int - puts "DEBUG: Node #{i}: first_int = #{first_int}, hex = 0x#{first_int.to_s(16)}" if ENV['XPATH_DEBUG'] - - # Type detection strategy: - # - Attribute nodes have node_type (int, value 1) as first 4 bytes - # - Element nodes have name pointer (8 bytes) as first field - # - # We check the first 4 bytes. If it's exactly 1, it's an attribute. - # Otherwise, try to convert as element, and if that fails, try as attribute. - - if first_int == 1 - # Attribute node (node_type == TAURUS_NODE_ATTRIBUTE) - # struct TaurusAttributeNode: node_type(4+4pad), name(8), value(8), ns_uri(8), owner(8) - begin - value_ptr = node_ptr.get_pointer(16) # offset 16 = value field - attr_value = value_ptr.null? ? "" : value_ptr.read_string - result << attr_value - rescue => e - warn "Warning: Failed to read attribute at index #{i}: #{e.message}" if $DEBUG - next - end - else - # Element node - call element_from_ptr to fully hydrate with children/attributes - begin - # Use element_from_ptr to properly populate element with all structure - elem = element_from_ptr(node_ptr, doc_ptr) - if elem - result << elem - else - warn "Warning: Element at index #{i} could not be converted" if $DEBUG - end - rescue => e - warn "Warning: Failed to convert element at index #{i}: #{e.message}" if $DEBUG - next - end - end - end - - result - - else - nil - end - end - end - end - end -end \ No newline at end of file diff --git a/lib/taurus/ffi/errors.rb b/lib/taurus/ffi/errors.rb deleted file mode 100644 index f0aa195..0000000 --- a/lib/taurus/ffi/errors.rb +++ /dev/null @@ -1,109 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module FFI - # ======================================================================== - # Error Handling - # ======================================================================== - - module ErrorHandling - # Map error codes to symbols - ERROR_CODE_SYMBOLS = { - ErrorCode::NULL_INPUT => :null_input, - ErrorCode::EMPTY_INPUT => :empty_input, - ErrorCode::PARSE_FAILED => :parse_failed, - ErrorCode::INVALID_XML => :invalid_xml, - ErrorCode::UNCLOSED_TAG => :unclosed_tag, - ErrorCode::INVALID_ATTR => :invalid_attr, - ErrorCode::ENCODING => :encoding, - ErrorCode::NAMESPACE => :namespace, - ErrorCode::MALFORMED => :malformed, - ErrorCode::XPATH_SYNTAX => :xpath_syntax, - ErrorCode::XPATH_FUNCTION => :xpath_function, - ErrorCode::XPATH_TYPE_MISMATCH => :xpath_type_mismatch, - ErrorCode::XPATH_NAMESPACE => :xpath_namespace, - ErrorCode::XPATH_UNKNOWN_AXIS => :xpath_unknown_axis, - ErrorCode::EVAL_CONTEXT => :eval_context, - ErrorCode::EVAL_ARGUMENT => :eval_argument, - ErrorCode::EVAL_OVERFLOW => :eval_overflow, - ErrorCode::OUT_OF_MEMORY => :out_of_memory, - ErrorCode::INTERNAL => :internal - }.freeze - - # Check for errors after FFI call and raise appropriate Ruby exception - # @raise [ParseError] if a parse error occurred - # @raise [XPathError] if an XPath error occurred - # @raise [Error] for other errors - def self.check_error! - error_msg = Taurus::FFI.taurus_last_error - return if error_msg.nil? || error_msg.empty? - - error_code = Taurus::FFI.taurus_last_error_code - line = Taurus::FFI.taurus_parse_error_line || 0 - column = Taurus::FFI.taurus_parse_error_column || 0 - context = Taurus::FFI.taurus_error_context - byte_offset = Taurus::FFI.taurus_error_byte_offset || 0 - - # Convert to symbol - code_symbol = ERROR_CODE_SYMBOLS[error_code] || :unknown - - Taurus::FFI.taurus_clear_error - - # Determine exception type based on error code - exception_class = case error_code - when ErrorCode::NULL_INPUT, ErrorCode::EMPTY_INPUT, - ErrorCode::UNCLOSED_TAG, ErrorCode::INVALID_ATTR, - ErrorCode::ENCODING, ErrorCode::NAMESPACE, - ErrorCode::MALFORMED, ErrorCode::PARSE_FAILED, - ErrorCode::INVALID_XML - Taurus::ParseError - when ErrorCode::XPATH_SYNTAX, ErrorCode::XPATH_FUNCTION, - ErrorCode::XPATH_TYPE_MISMATCH, ErrorCode::XPATH_NAMESPACE, - ErrorCode::XPATH_UNKNOWN_AXIS - Taurus::XPathError - when ErrorCode::EVAL_CONTEXT, ErrorCode::EVAL_ARGUMENT, - ErrorCode::EVAL_OVERFLOW - Taurus::EvaluationError - else - Taurus::Error - end - - # Create exception with all attributes - exception = exception_class.new( - error_msg, - code: code_symbol, - line: line, - column: column, - byte_offset: byte_offset, - context: context - ) - - raise exception - end - - # Wrap FFI call with error checking - # @yield block to execute - # @return result of block - # @raise [ParseError, Error] if error occurred - def self.with_error_check - result = yield - check_error! if result.nil? || (result.respond_to?(:null?) && result.null?) - result - end - end - - # ======================================================================== - # Convenience Methods - # ======================================================================== - - # Check for errors and raise if any - def self.check_error! - ErrorHandling.check_error! - end - - # Wrap call with error checking - def self.with_error_check(&block) - ErrorHandling.with_error_check(&block) - end - end -end \ No newline at end of file diff --git a/lib/taurus/ffi/library.rb b/lib/taurus/ffi/library.rb deleted file mode 100644 index cce6ebe..0000000 --- a/lib/taurus/ffi/library.rb +++ /dev/null @@ -1,120 +0,0 @@ -# frozen_string_literal: true - -require 'ffi' - -module Taurus - module FFI - extend ::FFI::Library - - # Dynamic library loading - # Try to find libtaurus in common locations - lib_paths = [ - File.expand_path('../../../build/lib/libtaurus.dylib', __dir__), # Build directory (macOS) - File.expand_path('../../../build/lib/libtaurus.so', __dir__), # Build directory (Linux) - File.expand_path('../../../lib/libtaurus.dylib', __dir__), # Installed location (macOS) - File.expand_path('../../../lib/libtaurus.so', __dir__), # Installed location (Linux) - 'taurus', # System library path - ] - - lib_path = lib_paths.find { |path| File.exist?(path) } || 'taurus' - ffi_lib lib_path - - # ======================================================================== - # Opaque Types - # ======================================================================== - - typedef :pointer, :taurus_document - typedef :pointer, :taurus_element - typedef :pointer, :taurus_attribute - typedef :pointer, :taurus_namespace - typedef :pointer, :taurus_xpath_result - - # ======================================================================== - # Version Functions - # ======================================================================== - - attach_function :taurus_version, [], :string - attach_function :taurus_version_components, [:pointer, :pointer, :pointer], :void - - # ======================================================================== - # Parse Options - # ======================================================================== - - attach_function :taurus_parse_options_init, [:pointer], :void - - # ======================================================================== - # Document Functions - # ======================================================================== - - attach_function :taurus_parse, [:string, :size_t], :taurus_document - attach_function :taurus_parse_with_options, [:string, :size_t, :pointer], :taurus_document - attach_function :taurus_document_free, [:taurus_document], :void - attach_function :taurus_document_root, [:taurus_document], :taurus_element - attach_function :taurus_document_encoding, [:taurus_document], :string - - # ======================================================================== - # Element Functions - # ======================================================================== - - attach_function :taurus_element_name, [:taurus_element], :string - attach_function :taurus_element_namespace, [:taurus_element], :string - attach_function :taurus_element_prefix, [:taurus_element], :string - attach_function :taurus_element_text, [:taurus_element], :string - attach_function :taurus_element_parent, [:taurus_element], :taurus_element - attach_function :taurus_element_child_count, [:taurus_element], :size_t - attach_function :taurus_element_child, [:taurus_element, :size_t], :taurus_element - - # ======================================================================== - # Attribute Functions - # ======================================================================== - - attach_function :taurus_element_attribute_count, [:taurus_element], :size_t - attach_function :taurus_element_attribute, [:taurus_element, :size_t], :taurus_attribute - attach_function :taurus_element_get_attribute, [:taurus_element, :string], :string - attach_function :taurus_element_has_attribute, [:taurus_element, :string], :int - attach_function :taurus_attribute_name, [:taurus_attribute], :string - attach_function :taurus_attribute_value, [:taurus_attribute], :string - attach_function :taurus_attribute_namespace, [:taurus_attribute], :string - - # ======================================================================== - # Namespace Functions - # ======================================================================== - - attach_function :taurus_element_namespace_count, [:taurus_element], :size_t - attach_function :taurus_element_namespace_decl, [:taurus_element, :size_t], :taurus_namespace - attach_function :taurus_element_resolve_namespace, [:taurus_element, :string], :string - attach_function :taurus_namespace_prefix, [:taurus_namespace], :string - attach_function :taurus_namespace_uri, [:taurus_namespace], :string - - # ======================================================================== - # XPath Functions - # ======================================================================== - - attach_function :taurus_xpath_eval, [:taurus_document, :string, :size_t], :taurus_xpath_result - attach_function :taurus_xpath_eval_with_context, - [:taurus_document, :taurus_element, :string, :size_t], - :taurus_xpath_result - attach_function :taurus_xpath_result_free, [:taurus_xpath_result], :void - attach_function :taurus_xpath_result_get_type, [:taurus_xpath_result], :int - attach_function :taurus_xpath_result_as_boolean, [:taurus_xpath_result], :int - attach_function :taurus_xpath_result_as_number, [:taurus_xpath_result], :double - attach_function :taurus_xpath_result_as_string, [:taurus_xpath_result], :string - attach_function :taurus_xpath_result_nodeset_size, [:taurus_xpath_result], :size_t - attach_function :taurus_xpath_result_nodeset_get, [:taurus_xpath_result, :size_t], :taurus_element - attach_function :taurus_xpath_function_supported, [:string], :int - attach_function :taurus_xpath_supported_functions, [], :pointer - - # ======================================================================== - # Error Functions - # ======================================================================== - - attach_function :taurus_last_error, [], :string - attach_function :taurus_last_error_code, [], :int - attach_function :taurus_error_string, [:int], :string - attach_function :taurus_clear_error, [], :void - attach_function :taurus_parse_error_line, [], :int - attach_function :taurus_parse_error_column, [], :int - attach_function :taurus_error_context, [], :string - attach_function :taurus_error_byte_offset, [], :size_t - end -end \ No newline at end of file diff --git a/lib/taurus/ffi/memory.rb b/lib/taurus/ffi/memory.rb deleted file mode 100644 index c7f5e39..0000000 --- a/lib/taurus/ffi/memory.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'ffi' - -module Taurus - module FFI - # ======================================================================== - # Memory Management with AutoPointer - # ======================================================================== - - # AutoPointer for taurus_document - automatically frees when GC'd - class DocumentPointer < ::FFI::AutoPointer - def self.release(ptr) - Taurus::FFI.taurus_document_free(ptr) unless ptr.null? - end - end - - # AutoPointer for taurus_xpath_result - automatically frees when GC'd - class XPathResultPointer < ::FFI::AutoPointer - def self.release(ptr) - Taurus::FFI.taurus_xpath_result_free(ptr) unless ptr.null? - end - end - - # ======================================================================== - # Memory Management Helpers - # ======================================================================== - - module MemoryHelpers - # Wrap a document pointer with AutoPointer for automatic cleanup - # @param ptr [FFI::Pointer] the document pointer from C - # @return [DocumentPointer] wrapped pointer that auto-frees - def self.wrap_document(ptr) - return nil if ptr.null? - DocumentPointer.new(ptr) - end - - # Wrap an XPath result pointer with AutoPointer for automatic cleanup - # @param ptr [FFI::Pointer] the result pointer from C - # @return [XPathResultPointer] wrapped pointer that auto-frees - def self.wrap_xpath_result(ptr) - return nil if ptr.null? - XPathResultPointer.new(ptr) - end - - # Free a string allocated by C - # @param str_ptr [FFI::Pointer] pointer to C-allocated string - # @return [String, nil] the string content before freeing - def self.free_c_string(str_ptr) - return nil if str_ptr.null? - str = str_ptr.read_string - # Note: According to taurus_xpath_result_as_string docs, - # caller must free with free(). FFI handles this via MemoryPointer. - ::FFI::MemoryPointer.from_string(str).free - str - end - end - end -end \ No newline at end of file diff --git a/lib/taurus/ffi/types.rb b/lib/taurus/ffi/types.rb deleted file mode 100644 index 8d35198..0000000 --- a/lib/taurus/ffi/types.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require 'ffi' - -module Taurus - module FFI - # ======================================================================== - # XPath Result Types - # ======================================================================== - - # XPath result type enumeration - module XPathResultType - BOOLEAN = 0 # Boolean result (true/false) - NUMBER = 1 # Number result (double) - STRING = 2 # String result - NODESET = 3 # Node-set result (array of elements) - end - - # ======================================================================== - # Error Codes - # ======================================================================== - - # Error codes - matches taurus_error_code in types.h - module ErrorCode - # Success - OK = 0 - - # Parse errors (1xx) - XML parsing issues - NULL_INPUT = 1 - EMPTY_INPUT = 2 - PARSE_FAILED = 3 - INVALID_XML = 4 - UNCLOSED_TAG = 100 - INVALID_ATTR = 101 - ENCODING = 102 - NAMESPACE = 103 - MALFORMED = 104 - - # XPath errors (2xx) - Query syntax and semantics - XPATH_SYNTAX = 200 - XPATH_FUNCTION = 201 - XPATH_TYPE_MISMATCH = 202 - XPATH_NAMESPACE = 203 - XPATH_UNKNOWN_AXIS = 204 - - # Evaluation errors (3xx) - Runtime issues - EVAL_CONTEXT = 300 - EVAL_ARGUMENT = 301 - EVAL_OVERFLOW = 302 - - # Generic errors (9xx) - OUT_OF_MEMORY = 900 - INTERNAL = 999 - end - - # ======================================================================== - # Parse Options Structure - # ======================================================================== - - # XML parse options - class ParseOptions < ::FFI::Struct - layout :strict, :int, # Strict XML validation (1=strict, 0=lenient) - :preserve_whitespace, :int, # Preserve whitespace-only text nodes - :track_positions, :int # Track line/column positions for errors - - # Initialize with default values - def self.default - opts = new - opts[:strict] = 1 - opts[:preserve_whitespace] = 0 - opts[:track_positions] = 0 - opts - end - end - - # ======================================================================== - # Helper Methods - # ======================================================================== - - # Convert XPath result type integer to symbol - # @param type_int [Integer] the result type as integer - # @return [Symbol] :boolean, :number, :string, or :nodeset - def self.xpath_result_type_to_sym(type_int) - case type_int - when XPathResultType::BOOLEAN then :boolean - when XPathResultType::NUMBER then :number - when XPathResultType::STRING then :string - when XPathResultType::NODESET then :nodeset - else :unknown - end - end - - # Convert error code integer to symbol - # @param code_int [Integer] the error code as integer - # @return [Symbol] error code symbol - def self.error_code_to_sym(code_int) - case code_int - when ErrorCode::OK then :ok - # Parse errors - when ErrorCode::NULL_INPUT then :null_input - when ErrorCode::EMPTY_INPUT then :empty_input - when ErrorCode::PARSE_FAILED then :parse_failed - when ErrorCode::INVALID_XML then :invalid_xml - when ErrorCode::UNCLOSED_TAG then :unclosed_tag - when ErrorCode::INVALID_ATTR then :invalid_attr - when ErrorCode::ENCODING then :encoding - when ErrorCode::NAMESPACE then :namespace - when ErrorCode::MALFORMED then :malformed - # XPath errors - when ErrorCode::XPATH_SYNTAX then :xpath_syntax - when ErrorCode::XPATH_FUNCTION then :xpath_function - when ErrorCode::XPATH_TYPE_MISMATCH then :xpath_type_mismatch - when ErrorCode::XPATH_NAMESPACE then :xpath_namespace - when ErrorCode::XPATH_UNKNOWN_AXIS then :xpath_unknown_axis - # Evaluation errors - when ErrorCode::EVAL_CONTEXT then :eval_context - when ErrorCode::EVAL_ARGUMENT then :eval_argument - when ErrorCode::EVAL_OVERFLOW then :eval_overflow - # Generic errors - when ErrorCode::OUT_OF_MEMORY then :out_of_memory - when ErrorCode::INTERNAL then :internal - else :unknown - end - end - end -end \ No newline at end of file diff --git a/lib/taurus/node.rb b/lib/taurus/node.rb deleted file mode 100644 index 90d8dd4..0000000 --- a/lib/taurus/node.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -module Taurus - # Base class for all XML nodes in the document tree. - # - # Node provides the foundation for {Element}, {Document}, and other node types. - # It maintains the node's value and parent relationship in the tree structure. - # - # @abstract Subclass and override methods to create specific node types - # - # @example Direct instantiation (typically not used directly) - # node = Taurus::Node.new("value") - # node.value # => "value" - # - # @see Element - # @see Document - class Node - # @return [String, Symbol, nil] the node's value (name for elements, content for text) - # @return [Element, nil] the parent element of this node - attr_accessor :value, :parent - - # Create a new node with the given value - # - # @param value [String, Symbol, nil] the node's value - # - # @example - # node = Taurus::Node.new("content") - def initialize(value) - @value = value - @parent = nil - end - - # Check equality with another node based on value - # - # Two nodes are equal if they are both Node instances and have the same value. - # Subclasses may override this to check additional attributes. - # - # @param other [Node] the node to compare with - # @return [Boolean] true if nodes are equal - # - # @example - # node1 = Taurus::Node.new("test") - # node2 = Taurus::Node.new("test") - # node1 == node2 # => true - def eql?(other) - return false unless other.is_a?(Node) - value == other.value - end - alias == eql? - end -end \ No newline at end of file diff --git a/lib/taurus/node_set.rb b/lib/taurus/node_set.rb deleted file mode 100644 index 8f6492a..0000000 --- a/lib/taurus/node_set.rb +++ /dev/null @@ -1,161 +0,0 @@ -# frozen_string_literal: true - -require 'set' - -module Taurus - # Collection of nodes returned by XPath queries. - # - # NodeSet provides an Enumerable interface to a collection of XML nodes. - # It is typically returned by XPath queries and supports standard array-like - # operations while maintaining the context of the query. - # - # @example Access nodes from XPath - # doc = Taurus.parse('') - # items = doc.xpath('//item') - # items.size # => 2 - # items.first # => - # - # @example Iterate over nodes - # doc.xpath('//item').each do |item| - # puts item.name - # end - # - # @example Convert to array - # nodes = doc.xpath('//item').to_a - # - # @see Document#xpath - # @see Element#xpath - class NodeSet - include Enumerable - - # @return [Array] the collection of nodes - # @return [Element, Document, nil] the context node for the query - attr_reader :nodes, :context - - # Create a new NodeSet - # - # @param nodes [Array, Element, nil] nodes to include in the set - # @param context [Element, Document, nil] the context node for the query - # - # @example Create from array - # nodes = [elem1, elem2] - # node_set = Taurus::NodeSet.new(nodes) - # - # @example Create from single node - # node_set = Taurus::NodeSet.new(elem) - def initialize(nodes = [], context = nil) - @nodes = Array(nodes) - @context = context - end - - # Iterate over each node in the set - # - # @yieldparam node [Element] each node in the set - # @return [Enumerator] if no block given - # - # @example - # node_set.each { |node| puts node.name } - def each(&block) - @nodes.each(&block) - end - - # Return the number of nodes in the set - # - # @return [Integer] the number of nodes - # - # @example - # doc.xpath('//item').size # => 3 - def size - @nodes.size - end - - # Check if the node set is empty - # - # @return [Boolean] true if no nodes in set - # - # @example - # doc.xpath('//nonexistent').empty? # => true - def empty? - @nodes.empty? - end - - # Return the first node in the set - # - # @return [Element, nil] the first node or nil if empty - # - # @example - # doc.xpath('//item').first # => - def first - @nodes.first - end - - # Return the last node in the set - # - # @return [Element, nil] the last node or nil if empty - # - # @example - # doc.xpath('//item').last # => - def last - @nodes.last - end - - # Access node by index - # - # @param index [Integer] the index (0-based) - # @return [Element, nil] the node at index or nil - # - # @example - # nodes = doc.xpath('//item') - # nodes[0] # => first item - # nodes[1] # => second item - def [](index) - @nodes[index] - end - - # Convert the node set to an array - # - # @return [Array] array of nodes - # - # @example - # doc.xpath('//item').to_a # => [, , ...] - def to_a - @nodes - end - - # Remove duplicate nodes by object identity - # - # This method modifies the node set in-place to remove duplicate nodes - # based on their native object identity. Useful when union operations - # may produce duplicate references. - # - # @return [NodeSet] self - # - # @example - # node_set.uniq_by_native - # node_set.size # => reduced if duplicates removed - def uniq_by_native - # Remove duplicates by object identity - seen = Set.new - unique_nodes = [] - @nodes.each do |node| - native = node.respond_to?(:native) ? node.native : node - unless seen.include?(native) - seen.add(native) - unique_nodes << node - end - end - @nodes = unique_nodes - self - end - - # Return a string representation of the node set - # - # @return [String] a debug-friendly representation - # - # @example - # node_set.inspect # => "#" - def inspect - "#" - end - end -end diff --git a/lib/taurus/xml.rb b/lib/taurus/xml.rb new file mode 100644 index 0000000..112d850 --- /dev/null +++ b/lib/taurus/xml.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Taurus + module XML + autoload :FFI, "taurus/xml/ffi" + autoload :Node, "taurus/xml/node" + autoload :Element, "taurus/xml/element" + autoload :Text, "taurus/xml/text" + autoload :Comment, "taurus/xml/comment" + autoload :CDATA, "taurus/xml/cdata" + autoload :ProcessingInstruction, "taurus/xml/processing_instruction" + autoload :Attr, "taurus/xml/attr" + autoload :Namespace, "taurus/xml/namespace" + autoload :Document, "taurus/xml/document" + autoload :DocumentFragment, "taurus/xml/document_fragment" + autoload :DocType, "taurus/xml/doc_type" + autoload :NodeSet, "taurus/xml/node_set" + autoload :Searchable, "taurus/xml/searchable" + autoload :ParseOptions, "taurus/xml/parse_options" + autoload :CssToXPath, "taurus/xml/css_to_xpath" + autoload :SAX, "taurus/xml/sax" + require "taurus/xml/c14n" # module-level helper, eager load + + class Error < StandardError; end + class ParseError < Error; end + class XPathError < Error; end + class UseAfterFreeError < Error; end + end +end diff --git a/lib/taurus/xml/attr.rb b/lib/taurus/xml/attr.rb new file mode 100644 index 0000000..f5bce9b --- /dev/null +++ b/lib/taurus/xml/attr.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Lightweight attribute wrapper. libtaurus v0.4.4 doesn't expose per-attribute +# pointers (no TaurusAttribute accessors in the public API), so an Attr is a +# (name, value, parent_element) triple. Mutation goes through the parent +# element via #[]. Once upstream adds attribute-level accessors, this can +# become a pointer-backed wrapper. + +class Taurus::XML::Attr + attr_reader :name, :value, :element + + def initialize(name, value, element) + @name = name + @value = value + @element = element + end + + def value=(new_value) + @element[@name] = new_value + @value = new_value + end + + def namespace + nil # upstream gap: per-attribute namespace not exposed + end + + def remove + @element.remove_attribute(@name) + end + + def to_s; @value; end + def to_str; @value; end + + def ==(other) + other.is_a?(Taurus::XML::Attr) && + @name == other.name && @value == other.value && + @element == other.element + end + + def inspect + "#<#{self.class.name} name=#{@name.inspect} value=#{@value.inspect}>" + end +end diff --git a/lib/taurus/xml/c14n.rb b/lib/taurus/xml/c14n.rb new file mode 100644 index 0000000..fce234d --- /dev/null +++ b/lib/taurus/xml/c14n.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Taurus + module XML + # @api private + # + # Builds a NULL-terminated `const char**` from a Ruby array of strings, + # suitable for passing to taurus_c14n_canonicalize_ex's + # `inclusive_ns_prefixes` argument. Returns [pointer, anchor] where + # anchor is the underlying MemoryPointer that the caller must keep + # alive (e.g. via local variable) for the duration of the FFI call. + def self.c14n_build_ns_pointer(inclusive_namespaces) + return [nil, nil] if inclusive_namespaces.nil? || inclusive_namespaces.empty? + strs = Array(inclusive_namespaces).map(&:to_s) + ptr_size = ::FFI.type_size(:pointer) + buffer = ::FFI::MemoryPointer.new(:pointer, strs.size + 1) + anchors = strs.map { |s| ::FFI::MemoryPointer.from_string(s) } + anchors.each_with_index { |p, i| buffer.put_pointer(i * ptr_size, p) } + buffer.put_pointer(strs.size * ptr_size, nil) + [buffer, anchors] + end + end +end diff --git a/lib/taurus/xml/cdata.rb b/lib/taurus/xml/cdata.rb new file mode 100644 index 0000000..5c5a4b5 --- /dev/null +++ b/lib/taurus/xml/cdata.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Taurus::XML::CDATA < Taurus::XML::Text + def name; "#cdata-section"; end + + def content + Taurus::XML::FFI.taurus_cdata_node_get_content(@c_ptr) + end + + def content=(new_content) + status = Taurus::XML::FFI.taurus_cdata_node_set_content(@c_ptr, new_content.to_s) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + new_content + end +end diff --git a/lib/taurus/xml/comment.rb b/lib/taurus/xml/comment.rb new file mode 100644 index 0000000..7ad8862 --- /dev/null +++ b/lib/taurus/xml/comment.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Taurus::XML::Comment < Taurus::XML::Node + def name; "comment"; end + + def content + Taurus::XML::FFI.taurus_comment_node_get_content(@c_ptr) + end + + def content=(new_content) + status = Taurus::XML::FFI.taurus_comment_node_set_content(@c_ptr, new_content.to_s) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + new_content + end +end diff --git a/lib/taurus/xml/css_to_xpath.rb b/lib/taurus/xml/css_to_xpath.rb new file mode 100644 index 0000000..11adebb --- /dev/null +++ b/lib/taurus/xml/css_to_xpath.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +module Taurus + module XML + # Minimal CSS-to-XPath translator covering the common Nokogiri subset: + # + # tag → //tag + # * → //* + # .class → //*[contains(concat(' ',@class,' '),' class ')] + # #id → //*[@id='id'] + # [attr] → //*[@attr] + # [attr=val] → //*[@attr='val'] + # tag[attr] → //tag[@attr] + # tag[attr=val] → //tag[@attr='val'] + # parent > child → //parent/child + # ancestor descendant → //ancestor//descendant + # a, b → //a | //b + # tag:first-child → //tag[position()=1] + # tag:last-child → //tag[position()=last()] + # tag:not(simple) → //tag[not(self::simple)] + # + # For anything beyond this subset, raise ArgumentError. Callers can fall + # back to writing XPath directly via #xpath/#at_xpath. + module CssToXPath + COMMA_SPLIT = /\s*,\s*/.freeze + private_constant :COMMA_SPLIT + + # Token types in a single simple selector. + # tag (optional), then zero or more of: .class, #id, [attr...], :pseudo + TAG_RE = /\A(\*|[\w-]+)/.freeze + DOT_CLASS_RE = /\A\.([\w-]+)/.freeze + HASH_ID_RE = /\A#([\w-]+)/.freeze + BRACKET_RE = /\A\[([^\]]+)\]/.freeze + PSEUDO_RE = /\A:([\w-]+(?:\([^)]*\))?)/.freeze + + module_function + + def convert(rule) + rule.to_s.split(COMMA_SPLIT).map { |r| convert_one(r.strip) }.join(" | ") + end + + def convert_one(rule) + return "//*" if rule == "*" + + # Tokenize chain first (handles > and whitespace) + if rule =~ /\s/ || rule.include?(">") + return convert_chain(rule) + end + + convert_simple(rule, prefix: "//") + end + + # Parse a single simple selector into (tag, predicates) where + # predicates is an array of XPath fragments to be joined via [p1][p2]... + def parse_simple(part) + tag = "*" + preds = [] + + s = part.strip + if s =~ TAG_RE + tag = $1 + s = $' + end + + until s.empty? + case s + when DOT_CLASS_RE + preds << "contains(concat(' ',normalize-space(@class),' '),' #{$1} ')" + s = $' + when HASH_ID_RE + preds << "@id='#{$1}'" + s = $' + when BRACKET_RE + preds << convert_attrs($1) + s = $' + when PSEUDO_RE + preds << convert_pseudo($1) + s = $' + else + raise ArgumentError, + "unsupported CSS selector at #{s.inspect} (in #{part.inspect})" + end + end + + [tag, preds] + end + + def convert_simple(part, prefix:) + tag, preds = parse_simple(part) + "#{prefix}#{tag}#{preds.map { |p| "[#{p}]" }.join}" + end + private_class_method :convert_simple + + def convert_attrs(clause) + case clause.strip + when /\A(\w+)\z/ + "@#{$1}" + when /\A(\w+)=['"]?([^'"\]]+)['"]?\z/ + "@#{$1}='#{$2}'" + when /\A(\w+)~=['"]?([^'"\]]+)['"]?\z/ + "contains(concat(' ',normalize-space(@#{$1}),' '),' #{$2} ')" + when /\A(\w+)\^=['"]?([^'"\]]+)['"]?\z/ + "starts-with(@#{$1}, '#{$2}')" + when /\A(\w+)\$=['"]?([^'"\]]+)['"]?\z/ + "substring(@#{$1}, string-length(@#{$1}) - string-length('#{$2}') + 1) = '#{$2}'" + when /\A(\w+)\*=['"]?([^'"\]]+)['"]?\z/ + "contains(@#{$1}, '#{$2}')" + else + raise ArgumentError, "unsupported attribute selector: [#{clause}]" + end + end + private_class_method :convert_attrs + + def convert_pseudo(pseudo) + case pseudo + when "first-child" then "not(preceding-sibling::*)" + when "last-child" then "not(following-sibling::*)" + when "only-child" then "not(preceding-sibling::* or following-sibling::*)" + when "empty" then "not(node())" + when "root" then "not(parent::*)" + when /\Anot\((.+)\)\z/ + inner_tag, _ = parse_simple($1) + "not(self::#{inner_tag})" + else + raise ArgumentError, "unsupported pseudo-class: :#{pseudo}" + end + end + private_class_method :convert_pseudo + + # Tokenize chain into [sel, op, sel, op, sel, ...] where op is :child or + # :descendant. Then build XPath. + def convert_chain(rule) + tokens = tokenize_chain(rule) + build_chain_xpath(tokens) + end + private_class_method :convert_chain + + def tokenize_chain(rule) + tokens = [] + s = rule.strip + until s.empty? + if s.sub!(/\A\s*>\s*/, "") + tokens << :child + elsif s.sub!(/\A\s+/, "") + # whitespace descendant only counts if previous token is a string + tokens << :descendant if tokens.last.is_a?(String) + else + # Read one simple selector — keep going until whitespace or > or end + m = s.match(/\A[^>\s]+/) + raise ArgumentError, "couldn't parse CSS chain at: #{s.inspect}" unless m + tokens << m[0] + s = m.post_match + end + end + tokens + end + private_class_method :tokenize_chain + + def build_chain_xpath(tokens) + first = tokens.shift + raise ArgumentError, "empty CSS chain" unless first.is_a?(String) + + xpath = convert_simple(first, prefix: "//") + until tokens.empty? + op = tokens.shift + sel = tokens.shift + raise ArgumentError, "malformed CSS chain" unless sel.is_a?(String) + inner_tag, preds = parse_simple(sel) + connector = op == :child ? "/" : "//" + xpath = "#{xpath}#{connector}#{inner_tag}#{preds.map { |p| "[#{p}]" }.join}" + end + xpath + end + private_class_method :build_chain_xpath + end + end +end diff --git a/lib/taurus/xml/doc_type.rb b/lib/taurus/xml/doc_type.rb new file mode 100644 index 0000000..38c5b3b --- /dev/null +++ b/lib/taurus/xml/doc_type.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Taurus::XML::DocType + attr_reader :c_ptr, :document + + def initialize(c_ptr, document) + @c_ptr = c_ptr + @document = document + end + + def name + Taurus::XML::FFI.taurus_doctype_get_name(@c_ptr) + end + alias_method :node_name, :name + + def root_name + Taurus::XML::FFI.taurus_doctype_get_root_name(@c_ptr) + end + + def public_id + Taurus::XML::FFI.taurus_doctype_get_public_id(@c_ptr) + end + + def system_id + Taurus::XML::FFI.taurus_doctype_get_system_id(@c_ptr) + end + + def internal_subset + Taurus::XML::FFI.taurus_doctype_get_internal_subset(@c_ptr) + end + + def external_id + pub = public_id + sys = system_id + return nil if pub.nil? && sys.nil? + parts = [] + parts << "PUBLIC \"#{pub}\"" if pub + parts << "SYSTEM \"#{sys}\"" if sys && pub.nil? + parts << "\"#{sys}\"" if pub && sys + parts.join(" ") + end + + def to_s + inner = internal_subset + parts = ["" + end + + def inspect + "#<#{self.class.name} name=#{name.inspect} public=#{public_id.inspect} system=#{system_id.inspect}>" + end +end diff --git a/lib/taurus/xml/document.rb b/lib/taurus/xml/document.rb new file mode 100644 index 0000000..cb03f99 --- /dev/null +++ b/lib/taurus/xml/document.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require "ffi" + +class Taurus::XML::Document + attr_reader :c_ptr, :wrapper_cache + + # @api private + # Internal flag container shared between the Document instance and its + # GC finalizer. Using a one-element Array because Procs close over + # variables by reference — mutating freed[0] is visible from both + # the explicit `free` path and the finalizer. This eliminates the + # double-free that FFI::AutoPointer's release proc caused when + # `Document#free` was called explicitly and then GC ran. + Freed = Struct.new(:state) # state: :alive | :freed + + def initialize(c_ptr = nil, freed = Freed.new(:alive)) + @c_ptr = c_ptr + @freed = freed + # Per-document weak-ref cache for Node wrappers, keyed on c_ptr + # address. Eliminates re-allocation when the same node is accessed + # repeatedly (e.g. via children, siblings, multiple xpath calls). + # Dies with the Document — no stale entries pointing at freed memory. + @wrapper_cache = ObjectSpace::WeakMap.new + end + + def self.parse(xml_or_io) + xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s + if xml.empty? + raise Taurus::XML::ParseError, "empty input" + end + status_ptr = ::FFI::MemoryPointer.new(:int) + raw = Taurus::XML::FFI.taurus_parse_string(xml, xml.bytesize, status_ptr) + if raw.null? + status = status_ptr.read_int + raise Taurus::XML::ParseError, + "taurus_parse_string failed (status=#{status})" + end + wrap(raw) + end + + def self.parse_file(path) + status_ptr = ::FFI::MemoryPointer.new(:int) + raw = Taurus::XML::FFI.taurus_parse_file(path, status_ptr) + if raw.null? + status = status_ptr.read_int + raise Taurus::XML::ParseError, + "taurus_parse_file failed (status=#{status})" + end + wrap(raw) + end + + # Convert a raw TaurusDocument pointer into a Ruby Document with safe + # GC lifetime management. The finalizer captures the raw address + # integer (not the Document or Pointer object — those would prevent + # GC) and shares a one-shot flag with the instance so explicit + # `#free` and the GC finalizer can never both call + # `taurus_document_free` on the same address. + def self.wrap(raw_address) + addr = raw_address.is_a?(::FFI::Pointer) ? raw_address.address : raw_address + ptr = ::FFI::Pointer.new(addr) + freed = Freed.new(:alive) + doc = new(ptr, freed) + ObjectSpace.define_finalizer(doc, finalizer(addr, freed)) + doc + end + + def self.finalizer(address, freed) + proc do + next if freed.state == :freed + freed.state = :freed + Taurus::XML::FFI.taurus_document_free(::FFI::Pointer.new(address)) + end + end + private_class_method :finalizer + + def root + raise Taurus::XML::UseAfterFreeError if @freed.state == :freed + return nil if @c_ptr.nil? + ptr = Taurus::XML::FFI.taurus_document_root(@c_ptr) + return nil if ptr.null? + Taurus::XML::Element.new(ptr, self) + end + + def create_element(name) + ptr = Taurus::XML::FFI.taurus_element_create(@c_ptr, name) + raise Taurus::XML::Error, "taurus_element_create failed" if ptr.null? + Taurus::XML::Element.new(ptr, self) + end + + def create_text_node(content) + ptr = Taurus::XML::FFI.taurus_text_node_create(@c_ptr, content.to_s) + raise Taurus::XML::Error, "taurus_text_node_create failed" if ptr.null? + Taurus::XML::Text.new(ptr, self) + end + + def create_comment(content) + ptr = Taurus::XML::FFI.taurus_comment_node_create(@c_ptr, content.to_s) + raise Taurus::XML::Error, "taurus_comment_node_create failed" if ptr.null? + Taurus::XML::Comment.new(ptr, self) + end + + def create_cdata(content) + ptr = Taurus::XML::FFI.taurus_cdata_node_create(@c_ptr, content.to_s) + raise Taurus::XML::Error, "taurus_cdata_node_create failed" if ptr.null? + Taurus::XML::CDATA.new(ptr, self) + end + + def create_processing_instruction(target, data = "") + ptr = Taurus::XML::FFI.taurus_pi_node_create(@c_ptr, target.to_s, data.to_s) + raise Taurus::XML::Error, "taurus_pi_node_create failed" if ptr.null? + Taurus::XML::ProcessingInstruction.new(ptr, self) + end + + def fragment(markup) + Taurus::XML::DocumentFragment.parse(markup, self) + end + + def dup + raw = Taurus::XML::FFI.taurus_document_copy(@c_ptr) + raise Taurus::XML::Error, "taurus_document_copy failed" if raw.null? + self.class.wrap(raw) + end + alias_method :clone, :dup + + def doctype + ptr = Taurus::XML::FFI.taurus_document_internal_subset(@c_ptr) + return nil if ptr.null? + Taurus::XML::DocType.new(ptr, self) + end + alias_method :internal_subset, :doctype + + def to_xml(indent: 0, no_decl: false, encoding: nil) + raise Taurus::XML::UseAfterFreeError if @freed.state == :freed + return "" if @c_ptr.nil? + opts, enc_ptr = build_serialize_options(indent: indent, no_decl: no_decl, encoding: encoding) + str_ptr = Taurus::XML::FFI.taurus_document_serialize(@c_ptr, opts.pointer) + return "" if str_ptr.null? + str_ptr.read_string.tap { |s| Taurus::XML::FFI.taurus_free_string(str_ptr) } + end + alias_method :to_s, :to_xml + alias_method :serialize, :to_xml + + def save(path, **opts) + opts_struct, enc_ptr = build_serialize_options( + indent: opts.fetch(:indent, 0), + no_decl: opts.fetch(:no_decl, false), + encoding: opts[:encoding]) + status = Taurus::XML::FFI.taurus_document_save_file(@c_ptr, path, opts_struct.pointer) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + self + end + + def canonicalize(version = Taurus::XML::FFI::C14N_1_0, + inclusive_namespaces = nil, + with_comments: false, + exclusive: false, + mode: nil) + raise Taurus::XML::UseAfterFreeError if @freed.state == :freed + return "" if @c_ptr.nil? + resolved_mode = mode || (exclusive ? Taurus::XML::FFI::C14N_MODE_EXCLUSIVE + : Taurus::XML::FFI::C14N_MODE_CANONICAL) + ns_ptr, _anchor = Taurus::XML.c14n_build_ns_pointer(inclusive_namespaces) + flags = with_comments ? 1 : 0 + str_ptr = Taurus::XML::FFI.taurus_c14n_canonicalize_ex( + @c_ptr, version, resolved_mode, ns_ptr, flags) + return "" if str_ptr.null? + str_ptr.read_string.tap { Taurus::XML::FFI.taurus_free_string(str_ptr) } + end + alias_method :c14n, :canonicalize + + def free + return if @freed.state == :freed + @freed.state = :freed + Taurus::XML::FFI.taurus_document_free(@c_ptr) unless @c_ptr.nil? + @c_ptr = nil + end + + def name; "document"; end + def document; self; end + def encoding + return nil if @c_ptr.nil? + Taurus::XML::FFI.taurus_document_encoding(@c_ptr) + end + + private + + def build_serialize_options(indent:, no_decl:, encoding:) + opts = Taurus::XML::FFI::SerializeOptions.new + opts[:indent] = indent.to_i + opts[:xml_declaration] = no_decl ? 0 : 1 + enc_ptr = nil + if encoding + enc_ptr = ::FFI::MemoryPointer.from_string(encoding.to_s) + opts[:encoding] = enc_ptr + end + [opts, enc_ptr] + end + + include Taurus::XML::Searchable +end diff --git a/lib/taurus/xml/document_fragment.rb b/lib/taurus/xml/document_fragment.rb new file mode 100644 index 0000000..7e91d18 --- /dev/null +++ b/lib/taurus/xml/document_fragment.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "ffi" + +# Wraps the synthetic "#document-fragment" element returned by +# taurus_parse_fragment. Children of this fragment are the parsed nodes; +# the fragment itself isn't part of any document tree but borrows its +# document's lifetime. +class Taurus::XML::DocumentFragment + attr_reader :document, :c_ptr + + def initialize(document, c_ptr) + @document = document + @c_ptr = c_ptr + end + + def self.parse(xml, document) + status_ptr = ::FFI::MemoryPointer.new(:int) + raw = Taurus::XML::FFI.taurus_parse_fragment( + xml.to_s, xml.to_s.bytesize, document.c_ptr, status_ptr) + if raw.null? + raise Taurus::XML::ParseError, + "taurus_parse_fragment failed (status=#{status_ptr.read_int})" + end + new(document, raw) + end + + def children + count = Taurus::XML::FFI.taurus_element_child_count(@c_ptr) + nodes = [] + ptr = Taurus::XML::FFI.taurus_node_first_child(@c_ptr) + until ptr.nil? || ptr.null? + nodes << Taurus::XML::Node.wrap(ptr, @document) + ptr = Taurus::XML::FFI.taurus_node_next_sibling(ptr) + end + Taurus::XML::NodeSet.new(@document, nodes) + end + + def name + "#document-fragment" + end +end diff --git a/lib/taurus/xml/element.rb b/lib/taurus/xml/element.rb new file mode 100644 index 0000000..a4c2b6c --- /dev/null +++ b/lib/taurus/xml/element.rb @@ -0,0 +1,278 @@ +# frozen_string_literal: true + +class Taurus::XML::Element < Taurus::XML::Node + def name + Taurus::XML::FFI.taurus_element_name(@c_ptr) + end + alias_method :node_name, :name + + def name=(new_name) + status = Taurus::XML::FFI.taurus_element_set_name(@c_ptr, new_name) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + new_name + end + alias_method :node_name=, :name= + + def content + Taurus::XML::FFI.taurus_element_text(@c_ptr) + end + + def content=(new_content) + status = Taurus::XML::FFI.taurus_element_set_text(@c_ptr, new_content.to_s) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + new_content + end + + def [](key) + Taurus::XML::FFI.taurus_element_attribute(@c_ptr, key.to_s) + end + alias_method :attr, :[] + alias_method :get_attribute, :[] + + def []=(key, value) + status = Taurus::XML::FFI.taurus_element_set_attribute(@c_ptr, key.to_s, value.to_s) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + value + end + alias_method :set_attribute, :[]= + + def key?(name) + !Taurus::XML::FFI.taurus_element_attribute(@c_ptr, name.to_s).nil? + end + alias_method :has_attribute?, :key? + + def remove_attribute(name) + status = Taurus::XML::FFI.taurus_element_remove_attribute(@c_ptr, name.to_s) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + self + end + alias_method :delete, :remove_attribute + + def keys + count = Taurus::XML::FFI.taurus_element_attribute_count(@c_ptr) + count.times.map { |i| Taurus::XML::FFI.taurus_element_attribute_name_at(@c_ptr, i) } + end + + def values + count = Taurus::XML::FFI.taurus_element_attribute_count(@c_ptr) + count.times.map { |i| Taurus::XML::FFI.taurus_element_attribute_value_at(@c_ptr, i) } + end + + def attributes + count = Taurus::XML::FFI.taurus_element_attribute_count(@c_ptr) + result = {} + count.times do |i| + name = Taurus::XML::FFI.taurus_element_attribute_name_at(@c_ptr, i) + value = Taurus::XML::FFI.taurus_element_attribute_value_at(@c_ptr, i) + result[name] = Taurus::XML::Attr.new(name, value, self) + end + result + end + + def attribute_nodes + count = Taurus::XML::FFI.taurus_element_attribute_count(@c_ptr) + count.times.map do |i| + name = Taurus::XML::FFI.taurus_element_attribute_name_at(@c_ptr, i) + value = Taurus::XML::FFI.taurus_element_attribute_value_at(@c_ptr, i) + Taurus::XML::Attr.new(name, value, self) + end + end + + def add_child(node) + status = Taurus::XML::FFI.taurus_element_append_child(@c_ptr, node.c_ptr) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + node + end + alias_method :<<, :add_child + + def prepend_child(node) + status = Taurus::XML::FFI.taurus_element_prepend_child(@c_ptr, node.c_ptr) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + node + end + + def add_next_sibling(node) + status = Taurus::XML::FFI.taurus_element_insert_after(@c_ptr, node.c_ptr) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + node + end + + def add_previous_sibling(node) + status = Taurus::XML::FFI.taurus_element_insert_before(@c_ptr, node.c_ptr) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + node + end + + def remove_child(node) + status = Taurus::XML::FFI.taurus_element_remove_child(@c_ptr, node.c_ptr) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + node + end + + def children=(node_or_nodes) + # Remove existing children, then attach the new ones in source order. + Taurus::XML::FFI.taurus_element_remove_children(@c_ptr) + Array(node_or_nodes).each { |n| add_child(n) } + end + + # Replace this element with +new_node+ in the parent's child list. + # +new_node+ must belong to the same document. Returns +new_node+. + def replace(new_node) + parent = self.parent + raise Taurus::XML::Error, "cannot replace a node with no parent" unless parent + add_next_sibling(new_node) + parent.remove_child(self) + new_node + end + + # Like #replace but returns self for chaining. + def swap(new_node) + replace(new_node) + self + end + + # Wrap this element in a new element parsed from +markup+ or a dup of + # +node+. The wrapper takes this element's place in the tree, and this + # element becomes its only child. Returns self for chaining. + def wrap(node_or_markup) + wrapper = + case node_or_markup + when Taurus::XML::Element then node_or_markup.dup + when String + frag_doc = Taurus::XML::Document.parse(node_or_markup) + frag_doc.root or raise Taurus::XML::Error, "wrap markup has no root element" + else + raise ArgumentError, "wrap expects a String or Element, got #{node_or_markup.class}" + end + + parent = self.parent + raise Taurus::XML::Error, "cannot wrap a node with no parent" unless parent + + # Insert wrapper at self's position, then move self into wrapper. + # add_child moves self (unlinks from old parent first), so no explicit + # remove_child needed — and trying to remove after the move corrupts + # the C tree (libtaurus silently handles non-child args badly). + add_next_sibling(wrapper) + wrapper.add_child(self) + self + end + + def dup + copy_ptr = Taurus::XML::FFI.taurus_element_copy(@c_ptr, @document.c_ptr) + raise Taurus::XML::Error, "taurus_element_copy failed" if copy_ptr.null? + Taurus::XML::Element.new(copy_ptr, @document) + end + alias_method :clone, :dup + + def add_child(node_or_markup) + case node_or_markup + when Taurus::XML::Node + status = Taurus::XML::FFI.taurus_element_append_child(@c_ptr, node_or_markup.c_ptr) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + node_or_markup + when String + frag = Taurus::XML::DocumentFragment.parse(node_or_markup, @document) + added = [] + frag.children.each do |n| + status = Taurus::XML::FFI.taurus_element_append_child(@c_ptr, n.c_ptr) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + added << n + end + Taurus::XML::NodeSet.new(@document, added) + else + raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}" + end + end + + def namespace + uri = Taurus::XML::FFI.taurus_element_namespace(@c_ptr) + return nil if uri.nil? || uri.empty? + Taurus::XML::Namespace.new(self, uri) + end + + def namespace_definitions + count = Taurus::XML::FFI.taurus_element_namespace_count(@c_ptr) + count.times.map do |i| + prefix = Taurus::XML::FFI.taurus_element_namespace_decl_prefix(@c_ptr, i) + uri = Taurus::XML::FFI.taurus_element_namespace_decl_uri(@c_ptr, i) + Taurus::XML::Namespace.new(self, uri, prefix: prefix) + end + end + + def namespaces + scopes = {} + node = self + while node.is_a?(Taurus::XML::Element) + node.namespace_definitions.each do |ns| + key = ns.prefix ? "xmlns:#{ns.prefix}" : "xmlns" + scopes[key] ||= ns.href + end + node = node.parent + end + scopes + end + + def to_xml(indent: 0, no_decl: false, encoding: nil) + opts = Taurus::XML::FFI::SerializeOptions.new + opts[:indent] = indent.to_i + opts[:xml_declaration] = no_decl ? 0 : 1 + enc_ptr = nil + if encoding + enc_ptr = ::FFI::MemoryPointer.from_string(encoding.to_s) + opts[:encoding] = enc_ptr + end + str_ptr = Taurus::XML::FFI.taurus_element_serialize(@c_ptr, opts.pointer) + return "" if str_ptr.null? + str_ptr.read_string.tap { Taurus::XML::FFI.taurus_free_string(str_ptr) } + end + + def canonicalize(version = Taurus::XML::FFI::C14N_1_0, + inclusive_namespaces = nil, + with_comments: false, + exclusive: false, + mode: nil) + resolved_mode = mode || (exclusive ? Taurus::XML::FFI::C14N_MODE_EXCLUSIVE + : Taurus::XML::FFI::C14N_MODE_CANONICAL) + ns_ptr, _anchor = Taurus::XML.c14n_build_ns_pointer(inclusive_namespaces) + flags = with_comments ? 1 : 0 + str_ptr = Taurus::XML::FFI.taurus_c14n_canonicalize_subtree_ex( + @c_ptr, version, resolved_mode, ns_ptr, flags) + return "" if str_ptr.null? + str_ptr.read_string.tap { Taurus::XML::FFI.taurus_free_string(str_ptr) } + end + alias_method :c14n, :canonicalize + + def add_namespace_definition(prefix, href) + status = Taurus::XML::FFI.taurus_element_add_namespace_definition( + @c_ptr, prefix.to_s, href.to_s) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + Taurus::XML::Namespace.new(self, href.to_s, prefix: prefix.nil? ? nil : prefix.to_s) + end + alias_method :add_namespace, :add_namespace_definition + + def default_namespace=(href) + status = Taurus::XML::FFI.taurus_element_set_default_namespace(@c_ptr, href.to_s) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + href + end + + def remove_namespace_definition(prefix) + status = Taurus::XML::FFI.taurus_element_remove_namespace_definition(@c_ptr, prefix.to_s) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + self + end +end diff --git a/lib/taurus/xml/ffi.rb b/lib/taurus/xml/ffi.rb new file mode 100644 index 0000000..990f3e9 --- /dev/null +++ b/lib/taurus/xml/ffi.rb @@ -0,0 +1,415 @@ +# frozen_string_literal: true + +require "ffi" + +module Taurus + module XML + module FFI + extend ::FFI::Library + + ffi_lib [ + ENV["TAURUS_LIB_PATH"], + File.expand_path("../../libtaurus.dylib", __dir__), + File.expand_path("../../libtaurus.so", __dir__), + "/usr/local/lib/libtaurus.dylib", + "/usr/local/lib/libtaurus.so", + "taurus", + ].compact + + typedef :pointer, :taurus_document + typedef :pointer, :taurus_element + typedef :pointer, :taurus_node_ref + typedef :pointer, :taurus_attribute + typedef :pointer, :taurus_doctype + typedef :pointer, :taurus_xpath_result + typedef :pointer, :taurus_xpath_var_set + typedef :pointer, :taurus_sax_parser + typedef :int, :taurus_status + + class SAXHandler < ::FFI::Struct + layout \ + :start_document, :pointer, + :end_document, :pointer, + :start_element, :pointer, + :end_element, :pointer, + :characters, :pointer, + :comment, :pointer, + :cdata, :pointer, + :processing_instruction, :pointer, + :start_prefix_mapping, :pointer, + :end_prefix_mapping, :pointer, + :error, :pointer + end + + class SerializeOptions < ::FFI::Struct + layout \ + :indent, :int, + :xml_declaration, :int, + :encoding, :pointer + end + + attach_function :taurus_version, [], :string + attach_function :taurus_version_components, [:pointer, :pointer, :pointer], :void + + attach_function :taurus_parse_string, + [:string, :size_t, :pointer], :taurus_document + attach_function :taurus_parse_string_inplace, + [:pointer, :size_t, :pointer], :taurus_document + attach_function :taurus_parse_string_with_encoding, + [:string, :size_t, :pointer], :taurus_document + attach_function :taurus_parse_file, + [:string, :pointer], :taurus_document + attach_function :taurus_load_file, + [:string, :pointer], :pointer + attach_function :taurus_document_free, + [:taurus_document], :void + attach_function :taurus_document_root, + [:taurus_document], :taurus_element + attach_function :taurus_document_encoding, + [:taurus_document], :string + attach_function :taurus_document_finalize_strings, + [:taurus_document], :int + attach_function :taurus_document_adopt_child, + [:taurus_document, :taurus_document], :void + attach_function :taurus_document_set_strict, + [:taurus_document, :int], :taurus_status + attach_function :taurus_document_get_strict, + [:taurus_document], :int + attach_function :taurus_document_freeze, + [:taurus_document], :taurus_status + attach_function :taurus_document_is_frozen, + [:taurus_document], :int + attach_function :taurus_set_strict_mode, [:int], :void + attach_function :taurus_get_strict_mode, [], :int + attach_function :taurus_set_max_depth, [:int], :void + attach_function :taurus_get_max_depth, [], :int + attach_function :taurus_xinclude_process, + [:taurus_document, :string], :taurus_status + attach_function :taurus_xinclude_is_include_element, + [:taurus_element], :int + attach_function :taurus_xinclude_is_fallback_element, + [:taurus_element], :int + attach_function :taurus_xinclude_get_href, [:taurus_element], :string + attach_function :taurus_xinclude_get_parse, [:taurus_element], :string + attach_function :taurus_xinclude_get_xpointer, [:taurus_element], :string + # taurus_xinclude_get_encoding is declared in taurus.h but not exported + # from the v0.4.4 dylib (likely same visibility bug as the serialize + # functions; see upstream issue #166). Re-add once #173 / v0.4.5 ships. + + attach_function :taurus_node_get_type, + [:taurus_node_ref], :int + attach_function :taurus_node_first_child, + [:taurus_node_ref], :taurus_node_ref + attach_function :taurus_node_last_child, + [:taurus_node_ref], :taurus_node_ref + attach_function :taurus_node_next_sibling, + [:taurus_node_ref], :taurus_node_ref + attach_function :taurus_node_previous_sibling, + [:taurus_node_ref], :taurus_node_ref + attach_function :taurus_node_child_count, + [:taurus_node_ref], :size_t + attach_function :taurus_node_as_element, + [:taurus_node_ref], :taurus_element + attach_function :taurus_element_as_node, + [:taurus_element], :taurus_node_ref + attach_function :taurus_node_get_binding_wrapper, + [:taurus_node_ref], :pointer + attach_function :taurus_node_set_binding_wrapper, + [:taurus_node_ref, :pointer], :void + attach_function :taurus_node_parent, + [:taurus_node_ref], :taurus_element + attach_function :taurus_node_unlink, + [:taurus_node_ref], :taurus_status + attach_function :taurus_node_line, + [:taurus_node_ref], :int + attach_function :taurus_node_compare, + [:taurus_node_ref, :taurus_node_ref], :int + + attach_function :taurus_text_node_get_content, + [:taurus_node_ref], :string + attach_function :taurus_comment_node_get_content, + [:taurus_node_ref], :string + attach_function :taurus_cdata_node_get_content, + [:taurus_node_ref], :string + attach_function :taurus_pi_node_get_target, + [:taurus_node_ref], :string + attach_function :taurus_pi_node_get_data, + [:taurus_node_ref], :string + attach_function :taurus_text_node_create, + [:taurus_document, :string], :taurus_node_ref + attach_function :taurus_comment_node_create, + [:taurus_document, :string], :taurus_node_ref + attach_function :taurus_cdata_node_create, + [:taurus_document, :string], :taurus_node_ref + attach_function :taurus_pi_node_create, + [:taurus_document, :string, :string], :taurus_node_ref + attach_function :taurus_text_node_set_content, + [:taurus_node_ref, :string], :taurus_status + attach_function :taurus_comment_node_set_content, + [:taurus_node_ref, :string], :taurus_status + attach_function :taurus_cdata_node_set_content, + [:taurus_node_ref, :string], :taurus_status + attach_function :taurus_pi_node_set_target, + [:taurus_node_ref, :string], :taurus_status + attach_function :taurus_pi_node_set_data, + [:taurus_node_ref, :string], :taurus_status + + attach_function :taurus_element_name, + [:taurus_element], :string + attach_function :taurus_element_text, + [:taurus_element], :string + attach_function :taurus_element_text_int, + [:taurus_element, :int], :int + attach_function :taurus_element_text_uint, + [:taurus_element, :uint], :uint + attach_function :taurus_element_text_double, + [:taurus_element, :double], :double + attach_function :taurus_element_text_float, + [:taurus_element, :float], :float + attach_function :taurus_element_text_bool, + [:taurus_element, :int], :int + attach_function :taurus_element_attribute, + [:taurus_element, :string], :string + attach_function :taurus_element_attribute_string, + [:taurus_element, :string, :string], :string + attach_function :taurus_element_attribute_int, + [:taurus_element, :string, :int], :int + attach_function :taurus_element_attribute_uint, + [:taurus_element, :string, :uint], :uint + attach_function :taurus_element_attribute_double, + [:taurus_element, :string, :double], :double + attach_function :taurus_element_attribute_float, + [:taurus_element, :string, :float], :float + attach_function :taurus_element_attribute_bool, + [:taurus_element, :string, :int], :int + # taurus_element_has_attribute is declared in taurus.h but not exported + # from the v0.4.4 dylib (likely visibility bug; see upstream #166). + # Ruby-level Element#key? will emulate via attribute lookup. + attach_function :taurus_element_attribute_count, + [:taurus_element], :size_t + attach_function :taurus_element_attribute_name_at, + [:taurus_element, :size_t], :string + attach_function :taurus_element_attribute_value_at, + [:taurus_element, :size_t], :string + attach_function :taurus_element_remove_all_attributes, + [:taurus_element], :taurus_status + + attach_function :taurus_element_child_count, + [:taurus_element], :size_t + attach_function :taurus_element_child, + [:taurus_element, :size_t], :taurus_element + attach_function :taurus_element_parent, + [:taurus_element], :taurus_element + attach_function :taurus_element_root, + [:taurus_element], :taurus_element + attach_function :taurus_element_child_value, + [:taurus_element], :string + attach_function :taurus_element_hash_value, + [:taurus_element], :size_t + attach_function :taurus_element_find_child, + [:taurus_element, :string], :taurus_element + attach_function :taurus_element_find_child_by_attr, + [:taurus_element, :string, :string, :string], :taurus_element + attach_function :taurus_element_next_sibling, + [:taurus_element, :string], :taurus_element + attach_function :taurus_element_previous_sibling, + [:taurus_element, :string], :taurus_element + attach_function :taurus_element_first_child, + [:taurus_element, :string], :taurus_element + attach_function :taurus_element_last_child, + [:taurus_element, :string], :taurus_element + attach_function :taurus_element_first_child_any, + [:taurus_element], :taurus_element + attach_function :taurus_element_last_child_any, + [:taurus_element], :taurus_element + attach_function :taurus_element_next_sibling_any, + [:taurus_element], :taurus_element + attach_function :taurus_element_previous_sibling_any, + [:taurus_element], :taurus_element + + attach_function :taurus_element_create, + [:taurus_document, :string], :taurus_element + attach_function :taurus_element_set_name, + [:taurus_element, :string], :taurus_status + attach_function :taurus_element_set_text, + [:taurus_element, :string], :taurus_status + attach_function :taurus_element_set_attribute, + [:taurus_element, :string, :string], :taurus_status + attach_function :taurus_element_set_attribute_bool, + [:taurus_element, :string, :int], :taurus_status + attach_function :taurus_element_set_attribute_double, + [:taurus_element, :string, :double], :taurus_status + attach_function :taurus_element_set_attribute_float, + [:taurus_element, :string, :float], :taurus_status + attach_function :taurus_element_set_attribute_int, + [:taurus_element, :string, :int], :taurus_status + attach_function :taurus_element_set_attribute_uint, + [:taurus_element, :string, :uint], :taurus_status + attach_function :taurus_element_remove_attribute, + [:taurus_element, :string], :taurus_status + attach_function :taurus_element_append_child, + [:taurus_element, :taurus_element], :taurus_status + attach_function :taurus_element_prepend_child, + [:taurus_element, :taurus_element], :taurus_status + attach_function :taurus_element_insert_before, + [:taurus_element, :taurus_element], :taurus_status + attach_function :taurus_element_insert_after, + [:taurus_element, :taurus_element], :taurus_status + attach_function :taurus_element_remove_child, + [:taurus_element, :taurus_element], :taurus_status + attach_function :taurus_element_remove_children, + [:taurus_element], :taurus_status + attach_function :taurus_element_append_copy, + [:taurus_element, :taurus_element], :taurus_element + attach_function :taurus_element_prepend_copy, + [:taurus_element, :taurus_element], :taurus_element + attach_function :taurus_element_insert_copy_before, + [:taurus_element, :taurus_element], :taurus_element + attach_function :taurus_element_insert_copy_after, + [:taurus_element, :taurus_element], :taurus_element + + attach_function :taurus_element_copy, + [:taurus_element, :taurus_document], :taurus_element + attach_function :taurus_document_copy, + [:taurus_document], :taurus_document + attach_function :taurus_node_get_xpath, + [:taurus_node_ref], :pointer + attach_function :taurus_parse_fragment, + [:string, :size_t, :taurus_document, :pointer], :taurus_element + + attach_function :taurus_element_namespace, + [:taurus_element], :string + attach_function :taurus_element_namespace_for_prefix, + [:taurus_element, :string], :string + attach_function :taurus_element_namespace_count, + [:taurus_element], :size_t + attach_function :taurus_element_namespace_decl_prefix, + [:taurus_element, :size_t], :string + attach_function :taurus_element_namespace_decl_uri, + [:taurus_element, :size_t], :string + attach_function :taurus_element_add_namespace_definition, + [:taurus_element, :string, :string], :taurus_status + attach_function :taurus_element_set_default_namespace, + [:taurus_element, :string], :taurus_status + attach_function :taurus_element_remove_namespace_definition, + [:taurus_element, :string], :taurus_status + attach_function :taurus_namespace_uri, [:string], :string + attach_function :taurus_namespace_prefix, [:string], :string + + attach_function :taurus_xpath_eval, + [:taurus_document, :taurus_element, :string], :taurus_xpath_result + attach_function :taurus_xpath_eval_with_vars, + [:taurus_document, :string, :taurus_xpath_var_set], :taurus_xpath_result + attach_function :taurus_xpath_eval_with_vars_context, + [:taurus_document, :taurus_element, :string, :taurus_xpath_var_set], + :taurus_xpath_result + attach_function :taurus_xpath_result_type, + [:taurus_xpath_result], :int + attach_function :taurus_xpath_result_count, + [:taurus_xpath_result], :size_t + attach_function :taurus_xpath_result_get, + [:taurus_xpath_result, :size_t], :taurus_element + attach_function :taurus_xpath_result_get_nodes, + [:taurus_xpath_result, :pointer, :size_t], :size_t + attach_function :taurus_xpath_result_boolean, + [:taurus_xpath_result], :int + attach_function :taurus_xpath_result_number, + [:taurus_xpath_result], :double + attach_function :taurus_xpath_result_string, + [:taurus_xpath_result], :pointer + attach_function :taurus_xpath_result_free, + [:taurus_xpath_result], :void + attach_function :taurus_xpath_function_supported, [:string], :int + attach_function :taurus_xpath_supported_functions, [], :pointer + + attach_function :taurus_xpath_variable_set_new, + [], :taurus_xpath_var_set + attach_function :taurus_xpath_variable_set_free, + [:taurus_xpath_var_set], :void + attach_function :taurus_xpath_variable_set_boolean, + [:taurus_xpath_var_set, :string, :int], :taurus_status + attach_function :taurus_xpath_variable_set_number, + [:taurus_xpath_var_set, :string, :double], :taurus_status + attach_function :taurus_xpath_variable_set_string, + [:taurus_xpath_var_set, :string, :string], :taurus_status + + attach_function :taurus_sax_parse, + [:string, :size_t, :pointer, :pointer], :int + attach_function :taurus_sax_parser_create, + [:pointer, :pointer], :taurus_sax_parser + attach_function :taurus_sax_parser_feed, + [:taurus_sax_parser, :string, :size_t, :int], :int + attach_function :taurus_sax_parser_free, + [:taurus_sax_parser], :void + attach_function :taurus_sax_parser_set_streaming, + [:taurus_sax_parser, :int], :int + + attach_function :taurus_document_serialize, + [:taurus_document, :pointer], :pointer + attach_function :taurus_element_serialize, + [:taurus_element, :pointer], :pointer + attach_function :taurus_document_save_file, + [:taurus_document, :string, :pointer], :taurus_status + attach_function :taurus_c14n_canonicalize, + [:taurus_document, :int, :int], :pointer + attach_function :taurus_c14n_canonicalize_subtree, + [:taurus_element, :int, :int], :pointer + attach_function :taurus_c14n_canonicalize_ex, + [:taurus_document, :int, :int, :pointer, :int], :pointer + attach_function :taurus_c14n_canonicalize_subtree_ex, + [:taurus_element, :int, :int, :pointer, :int], :pointer + + attach_function :taurus_document_internal_subset, + [:taurus_document], :taurus_doctype + attach_function :taurus_doctype_get_name, + [:taurus_doctype], :string + attach_function :taurus_doctype_get_root_name, + [:taurus_doctype], :string + attach_function :taurus_doctype_get_public_id, + [:taurus_doctype], :string + attach_function :taurus_doctype_get_system_id, + [:taurus_doctype], :string + attach_function :taurus_doctype_get_internal_subset, + [:taurus_doctype], :string + + attach_function :taurus_free_string, [:pointer], :void + attach_function :taurus_explicit_cleanup, [], :void + attach_function :taurus_set_memory_management_functions, + [:pointer, :pointer], :void + attach_function :taurus_get_memory_allocation_function, [], :pointer + attach_function :taurus_get_memory_deallocation_function, [], :pointer + attach_function :taurus_document_set_allocators, + [:taurus_document, :pointer, :pointer], :taurus_status + attach_function :taurus_status_string, [:taurus_status], :string + + TAURUS_OK = 0 + TAURUS_ERROR_MEMORY = -1 + TAURUS_ERROR_PARSE = -2 + TAURUS_ERROR_XPATH = -3 + TAURUS_ERROR_NULL_ARG = -4 + TAURUS_ERROR_INVALID_ARG = -5 + TAURUS_ERROR_NOT_FOUND = -6 + TAURUS_ERROR_IO = -7 + TAURUS_ERROR_NOT_IMPLEMENTED = -8 + + XPATH_NODESET = 0 + XPATH_BOOLEAN = 1 + XPATH_NUMBER = 2 + XPATH_STRING = 3 + + NODE_ELEMENT = 0 + NODE_TEXT = 1 + NODE_COMMENT = 2 + NODE_CDATA = 3 + NODE_PI = 4 + NODE_DOCTYPE = 5 + NODE_ATTRIBUTE = 6 + + C14N_1_0 = 0 + C14N_1_1 = 1 + + C14N_MODE_CANONICAL = 0 + C14N_MODE_EXCLUSIVE = 1 + end + end +end diff --git a/lib/taurus/xml/namespace.rb b/lib/taurus/xml/namespace.rb new file mode 100644 index 0000000..500da99 --- /dev/null +++ b/lib/taurus/xml/namespace.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# libtaurus's TaurusNamespace is `const char*` (the URI directly). The Ruby +# Namespace wrapper holds (element, uri, optional prefix). When the prefix +# isn't supplied (e.g. via Element#namespace), it can be derived by walking +# the element's namespace declarations via taurus_element_namespace_decl_*. + +class Taurus::XML::Namespace + attr_reader :element, :href, :prefix + + def initialize(element, href, prefix: :derive) + @element = element + @href = href + @prefix = prefix == :derive ? derive_prefix : prefix + end + + def document + @element&.document + end + + def ==(other) + other.is_a?(Taurus::XML::Namespace) && + @href == other.href && @prefix == other.prefix + end + + def inspect + "#<#{self.class.name} prefix=#{@prefix.inspect} href=#{@href.inspect}>" + end + + private + + def derive_prefix + return nil if @element.nil? + count = Taurus::XML::FFI.taurus_element_namespace_count(@element.c_ptr) + count.times do |i| + uri = Taurus::XML::FFI.taurus_element_namespace_decl_uri(@element.c_ptr, i) + next unless uri == @href + prefix = Taurus::XML::FFI.taurus_element_namespace_decl_prefix(@element.c_ptr, i) + return prefix.nil? || prefix.empty? ? nil : prefix + end + nil + end +end diff --git a/lib/taurus/xml/node.rb b/lib/taurus/xml/node.rb new file mode 100644 index 0000000..e2adfe4 --- /dev/null +++ b/lib/taurus/xml/node.rb @@ -0,0 +1,228 @@ +# frozen_string_literal: true + +class Taurus::XML::Node + attr_reader :c_ptr, :document + + def initialize(c_ptr, document, parent: nil) + @c_ptr = c_ptr + @document = document + @parent = parent + end + + def self.wrap(c_ptr, document, parent: nil) + # Per-document weak-ref cache. Returns the existing wrapper when the + # same c_ptr is wrapped twice (common in children/sibling walks, + # repeated xpath queries, traverse-then-access patterns). The cache + # dies with the document so no stale entries. + if document && (cached = document.wrapper_cache[c_ptr.address]) + return cached + end + + node = + case Taurus::XML::FFI.taurus_node_get_type(c_ptr) + when Taurus::XML::FFI::NODE_ELEMENT + Taurus::XML::Element.new(c_ptr, document, parent: parent) + when Taurus::XML::FFI::NODE_TEXT + Taurus::XML::Text.new(c_ptr, document, parent: parent) + when Taurus::XML::FFI::NODE_COMMENT + Taurus::XML::Comment.new(c_ptr, document, parent: parent) + when Taurus::XML::FFI::NODE_CDATA + Taurus::XML::CDATA.new(c_ptr, document, parent: parent) + when Taurus::XML::FFI::NODE_PI + Taurus::XML::ProcessingInstruction.new(c_ptr, document, parent: parent) + else + new(c_ptr, document, parent: parent) + end + + document.wrapper_cache[c_ptr.address] = node if document + node + end + + def name + raise NotImplementedError, "#{self.class}#name not implemented" + end + + def content + raise NotImplementedError, "#{self.class}#content not implemented" + end + alias_method :text, :content + alias_method :inner_text, :content + + def type + Taurus::XML::FFI.taurus_node_get_type(@c_ptr) + end + alias_method :node_type, :type + + def element?; type == Taurus::XML::FFI::NODE_ELEMENT; end + def text?; type == Taurus::XML::FFI::NODE_TEXT; end + def comment?; type == Taurus::XML::FFI::NODE_COMMENT; end + def cdata?; type == Taurus::XML::FFI::NODE_CDATA; end + def processing_instruction? + type == Taurus::XML::FFI::NODE_PI + end + alias_method :pi?, :processing_instruction? + + def parent + return @parent if @parent + ptr = Taurus::XML::FFI.taurus_node_parent(@c_ptr) + return nil if ptr.null? + Taurus::XML::Element.new(ptr, @document) + end + + def line + Taurus::XML::FFI.taurus_node_line(@c_ptr) + end + + def <=>(other) + return nil unless other.is_a?(Taurus::XML::Node) + return nil unless @document == other.document + Taurus::XML::FFI.taurus_node_compare(@c_ptr, other.c_ptr) + end + + def child + ptr = Taurus::XML::FFI.taurus_node_first_child(@c_ptr) + return nil if ptr.null? + Taurus::XML::Node.wrap(ptr, @document, parent: as_element_or_self) + end + + def children + nodes = [] + ptr = Taurus::XML::FFI.taurus_node_first_child(@c_ptr) + until ptr.nil? || ptr.null? + nodes << Taurus::XML::Node.wrap(ptr, @document, parent: as_element_or_self) + ptr = Taurus::XML::FFI.taurus_node_next_sibling(ptr) + end + Taurus::XML::NodeSet.new(@document, nodes) + end + + def next_sibling + ptr = Taurus::XML::FFI.taurus_node_next_sibling(@c_ptr) + return nil if ptr.null? + Taurus::XML::Node.wrap(ptr, @document, parent: @parent) + end + alias_method :next, :next_sibling + + def previous_sibling + ptr = Taurus::XML::FFI.taurus_node_previous_sibling(@c_ptr) + return nil if ptr.null? + Taurus::XML::Node.wrap(ptr, @document, parent: @parent) + end + alias_method :previous, :previous_sibling + + def first_element_child + ptr = Taurus::XML::FFI.taurus_node_first_child(@c_ptr) + until ptr.nil? || ptr.null? + node = Taurus::XML::Node.wrap(ptr, @document, parent: as_element_or_self) + return node if node.element? + ptr = Taurus::XML::FFI.taurus_node_next_sibling(ptr) + end + nil + end + + def last_element_child + children.reverse_each.find(&:element?) + end + + def element_children + children.select(&:element?) + end + alias_method :elements, :element_children + + def next_element + sibling = next_sibling + sibling = sibling.next_sibling until sibling.nil? || sibling.element? + sibling + end + + def previous_element + sibling = previous_sibling + sibling = sibling.previous_sibling until sibling.nil? || sibling.element? + sibling + end + + def unlink + status = Taurus::XML::FFI.taurus_node_unlink(@c_ptr) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + @parent = nil + self + end + alias_method :remove, :unlink + + # Walks the subtree in post-order DFS (matches Nokogiri's semantics). + # + # Specialized hot path: skips the intermediate NodeSet allocation that + # Element#children would create, walking via raw FFI calls and wrapping + # nodes directly. Saves one Array + one NodeSet allocation per parent + # node. For a tree of N nodes that's ~N fewer allocations on a full + # traversal. + # + # Still pays ~2 FFI calls per visited node (first_child + next_sibling). + # Beating Nokogiri on this benchmark needs C-side traverse with a + # callback (libtaurus #273); the per-node FFI cost is the floor. + def traverse + return enum_for(:traverse) unless block_given? + walk_post_order(@c_ptr, @document) { |n| yield n } + end + + def path + str_ptr = Taurus::XML::FFI.taurus_node_get_xpath(@c_ptr) + return nil if str_ptr.null? + str_ptr.read_string.tap { Taurus::XML::FFI.taurus_free_string(str_ptr) } + end + + def css_path + return nil if path.nil? + path.split("/").filter_map do |part| + next nil if part.empty? + part.gsub(/\[(\d+)\]/, ':nth-of-type(\1)') + end.join(" > ") + end + + def dup + elem_ptr = Taurus::XML::FFI.taurus_node_as_element(@c_ptr) + raise Taurus::XML::Error, "dup is only supported for element nodes" if elem_ptr.null? + copy_ptr = Taurus::XML::FFI.taurus_element_copy(elem_ptr, @document.c_ptr) + raise Taurus::XML::Error, "taurus_element_copy failed" if copy_ptr.null? + Taurus::XML::Element.new(copy_ptr, @document) + end + alias_method :clone, :dup + + def ==(other) + return false unless other.is_a?(Taurus::XML::Node) + @c_ptr == other.c_ptr + end + + def inspect + "#<#{self.class.name} ptr=#{c_ptr}>" + end + + protected + + def as_element_or_self + is_a?(Taurus::XML::Element) ? self : nil + end + + private + + # Walks the subtree in post-order DFS (matches Nokogiri's semantics). + # + # Specialized hot path for #traverse: skips the intermediate NodeSet + # allocation that Element#children would create, walking via raw FFI + # calls and wrapping nodes directly. Saves one Array + one NodeSet + # allocation per parent node. + # + # Still pays ~2 FFI calls per visited node (first_child + next_sibling). + # Beating Nokogiri on this benchmark needs C-side traverse with a + # callback (libtaurus #273); the per-node FFI cost is the floor. + def walk_post_order(ptr, doc, &block) + child_ptr = Taurus::XML::FFI.taurus_node_first_child(ptr) + until child_ptr.nil? || child_ptr.null? + walk_post_order(child_ptr, doc, &block) + child_ptr = Taurus::XML::FFI.taurus_node_next_sibling(child_ptr) + end + yield Taurus::XML::Node.wrap(ptr, doc) + end + + include Taurus::XML::Searchable +end diff --git a/lib/taurus/xml/node_set.rb b/lib/taurus/xml/node_set.rb new file mode 100644 index 0000000..303a893 --- /dev/null +++ b/lib/taurus/xml/node_set.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require "ffi" + +class Taurus::XML::NodeSet + include Enumerable + include Taurus::XML::Searchable + + attr_reader :document + + # Two construction modes: + # - eager: pass an Array of Nodes (e.g. Element#children builds one) + # - lazy: pass an FFI::Pointer to a TaurusXPathResult that this NodeSet + # will keep alive and free on GC. Each [i] / each call goes + # through taurus_xpath_result_get instead of pre-materializing. + def initialize(document, source = nil) + @document = document + case source + when ::FFI::Pointer + if source.null? + @result_ptr = nil + @array = [] + else + # Wrap in AutoPointer for automatic GC-time cleanup. NodeSet has no + # explicit #free method (Nokogiri doesn't either), so the AutoPointer + # double-free risk that Document#free hit doesn't apply here. + @result_ptr = ::FFI::AutoPointer.new(source, Taurus::XML::FFI.method(:taurus_xpath_result_free)) + @array = nil + end + when nil + @result_ptr = nil + @array = [] + else + @result_ptr = nil + @array = source.to_a + end + end + + def self.from_result(document, result_ptr) + new(document, result_ptr) + end + + def length + @result_ptr ? Taurus::XML::FFI.taurus_xpath_result_count(@result_ptr) : @array.length + end + alias_method :size, :length + + def empty? + length == 0 + end + + def [](idx) + if @result_ptr + return nil if idx < 0 || idx >= length + ptr = Taurus::XML::FFI.taurus_xpath_result_get(@result_ptr, idx) + return nil if ptr.null? + Taurus::XML::Node.wrap(ptr, @document) + else + @array[idx] + end + end + + def each + return enum_for(:each) unless block_given? + if @result_ptr + # Batch-fetch all node pointers in one FFI call (taurus_xpath_result_get_nodes, + # libtaurus v0.11.4) and wrap each. Saves N-1 FFI calls vs the per-index + # taurus_xpath_result_get loop. Wrappers are still cached per-Document via + # Node.wrap, so a re-iteration of the same NodeSet hits the cache. + n = length + if n > 0 + buf = ::FFI::MemoryPointer.new(:pointer, n) + begin + copied = Taurus::XML::FFI.taurus_xpath_result_get_nodes(@result_ptr, buf, n) + copied.times do |i| + ptr = buf.get_pointer(i * ::FFI.type_size(:pointer)) + next if ptr.null? + yield Taurus::XML::Node.wrap(ptr, @document) + end + ensure + buf.free + end + end + else + @array.each { |n| yield n } + end + self + end + + def to_a + return @array if @array + return [] if @result_ptr.nil? + out = [] + each { |n| out << n } + @array = out # memoize so subsequent to_a / each avoids re-fetch + out + end + alias_method :to_ary, :to_a + + def first(n = nil) + return self[0] if n.nil? + n.times.map { |i| self[i] }.take_while { |x| !x.nil? } + end + + def last + if @result_ptr + self[length - 1] + else + @array.last + end + end + + def inner_text + map(&:content).join + end + alias_method :text, :inner_text + + def xpath(*paths) + handler, _ns, _vars = parse_search_args(paths) + raise ArgumentError, "custom XPath handlers not supported" if handler + expr = paths.join(" | ") + accumulated = Taurus::XML::NodeSet.new(@document) + each do |node| + next unless node.is_a?(Taurus::XML::Element) + result_ptr = Taurus::XML::FFI.taurus_xpath_eval( + @document.c_ptr, node.c_ptr, expr) + next if result_ptr.null? + sub = Taurus::XML::NodeSet.send(:from_result, @document, result_ptr) + accumulated = merge_node_sets(accumulated, sub) + end + accumulated + end + + def inspect + to_a.inspect + end + + private + + def merge_node_sets(a, b) + Taurus::XML::NodeSet.new(@document, a.to_a + b.to_a) + end +end diff --git a/lib/taurus/xml/parse_options.rb b/lib/taurus/xml/parse_options.rb new file mode 100644 index 0000000..459d36c --- /dev/null +++ b/lib/taurus/xml/parse_options.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Taurus::XML::ParseOptions + DEFAULT_XML = 0 + RECOVER = 1 << 0 + NOERROR = 1 << 5 + NOWARNING = 1 << 6 + NOCDATA = 1 << 8 + STRICT = 1 << 18 + + attr_reader :options + + def initialize(options = DEFAULT_XML) + @options = options.to_i + end + + def strict?; !@options.zero?; end + def recover?; @options & RECOVER != 0; end +end diff --git a/lib/taurus/xml/processing_instruction.rb b/lib/taurus/xml/processing_instruction.rb new file mode 100644 index 0000000..30114b7 --- /dev/null +++ b/lib/taurus/xml/processing_instruction.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Taurus::XML::ProcessingInstruction < Taurus::XML::Node + def name + Taurus::XML::FFI.taurus_pi_node_get_target(@c_ptr) + end + alias_method :target, :name + + def content + Taurus::XML::FFI.taurus_pi_node_get_data(@c_ptr).to_s + end + + def target=(new_target) + status = Taurus::XML::FFI.taurus_pi_node_set_target(@c_ptr, new_target.to_s) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + new_target + end + + def data=(new_data) + status = Taurus::XML::FFI.taurus_pi_node_set_data(@c_ptr, new_data.to_s) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + new_data + end +end diff --git a/lib/taurus/xml/sax.rb b/lib/taurus/xml/sax.rb new file mode 100644 index 0000000..770aa9a --- /dev/null +++ b/lib/taurus/xml/sax.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "ffi" + +module Taurus + module XML + module SAX + autoload :Document, "taurus/xml/sax/document" + autoload :Parser, "taurus/xml/sax/parser" + end + end +end diff --git a/lib/taurus/xml/sax/document.rb b/lib/taurus/xml/sax/document.rb new file mode 100644 index 0000000..ca71ea5 --- /dev/null +++ b/lib/taurus/xml/sax/document.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class Taurus::XML::SAX::Document + # Called when an XML declaration is parsed. + def xmldecl(version, encoding, standalone) + end + + def start_document + end + + def end_document + end + + # Called at the beginning of an element. + # +attrs+ is an Array of [name, value] pairs in source order. + def start_element(name, attrs = []) + end + + def end_element(name) + end + + def characters(string) + end + + def comment(string) + end + + def cdata_block(string) + end + + def processing_instruction(name, content) + end + + def start_prefix_mapping(prefix, uri) + end + + def end_prefix_mapping(prefix) + end + + def warning(string) + end + + def error(message, line = 0, column = 0) + end +end diff --git a/lib/taurus/xml/sax/parser.rb b/lib/taurus/xml/sax/parser.rb new file mode 100644 index 0000000..3706b65 --- /dev/null +++ b/lib/taurus/xml/sax/parser.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require "ffi" + +class Taurus::XML::SAX::Parser + CHUNK_SIZE = 4096 + private_constant :CHUNK_SIZE + + attr_accessor :document, :encoding + + def initialize(handler = Taurus::XML::SAX::Document.new, encoding = nil) + @document = handler + @encoding = encoding + end + + # Parse a string, IO, or file path. Dispatches to parse_memory / + # parse_io / parse_file based on the argument type. + def parse(input) + case input + when String then parse_memory(input) + when ->(x) { x.respond_to?(:read) } then parse_io(input) + else + raise ArgumentError, "SAX parser expects a String or IO, got #{input.class}" + end + end + + def parse_memory(string) + string = string.dup.force_encoding("UTF-8") + handler_struct = build_handler_struct + rc = Taurus::XML::FFI.taurus_sax_parse( + string, string.bytesize, handler_struct.pointer, nil) + if rc != 0 + raise Taurus::XML::ParseError, + "taurus_sax_parse failed (rc=#{rc})" + end + self + end + + def parse_io(io) + handler_struct = build_handler_struct + parser_ptr = Taurus::XML::FFI.taurus_sax_parser_create( + handler_struct.pointer, nil) + if parser_ptr.null? + raise Taurus::XML::Error, "taurus_sax_parser_create failed" + end + begin + while (chunk = io.read(CHUNK_SIZE)) + rc = Taurus::XML::FFI.taurus_sax_parser_feed( + parser_ptr, chunk, chunk.bytesize, 0) + if rc != 0 + raise Taurus::XML::ParseError, "taurus_sax_parser_feed failed (rc=#{rc})" + end + end + # Final flush + rc = Taurus::XML::FFI.taurus_sax_parser_feed(parser_ptr, "", 0, 1) + if rc != 0 + raise Taurus::XML::ParseError, "taurus_sax_parser_feed (final) failed (rc=#{rc})" + end + ensure + Taurus::XML::FFI.taurus_sax_parser_free(parser_ptr) + end + self + end + + def parse_file(path) + File.open(path, "r") { |f| parse_io(f) } + end + + private + + # Build a TaurusSAXHandler struct populated with FFI::Function callbacks + # that dispatch to the Ruby handler. The struct (and its callbacks) are + # anchored against GC via the local variable for the duration of the + # synchronous parse call. + def build_handler_struct + s = Taurus::XML::FFI::SAXHandler.new + handler = @document # capture in closures + + s[:start_document] = callback(:void, [:pointer]) do |_| + handler.start_document + end + + s[:end_document] = callback(:void, [:pointer]) do |_| + handler.end_document + end + + s[:start_element] = callback(:void, [:pointer, :string, :pointer]) do |_, name, attrs_ptr| + attrs = walk_attr_array(attrs_ptr) + handler.start_element(name, attrs) + end + + s[:end_element] = callback(:void, [:pointer, :string]) do |_, name| + handler.end_element(name) + end + + s[:characters] = callback(:void, [:pointer, :pointer, :size_t]) do |_, text_ptr, len| + handler.characters(text_ptr.read_bytes(len).force_encoding("UTF-8")) + end + + s[:comment] = callback(:void, [:pointer, :string]) do |_, comment| + handler.comment(comment) + end + + s[:cdata] = callback(:void, [:pointer, :string]) do |_, cdata| + handler.cdata_block(cdata) + end + + s[:processing_instruction] = callback(:void, [:pointer, :string, :string]) do |_, target, data| + handler.processing_instruction(target, data) + end + + s[:start_prefix_mapping] = callback(:void, [:pointer, :string, :string]) do |_, prefix, uri| + handler.start_prefix_mapping(prefix, uri) + end + + s[:end_prefix_mapping] = callback(:void, [:pointer, :string]) do |_, prefix| + handler.end_prefix_mapping(prefix) + end + + s[:error] = callback(:void, [:pointer, :string, :int, :int]) do |_, msg, line, col| + handler.error(msg, line, col) + end + + s + end + + def callback(return_type, params, blocking: true, &block) + ::FFI::Function.new(return_type, params, blocking: blocking, &block) + end + + # The C `const char** attrs` is a NULL-terminated flat array of name/value + # pairs: [name1, value1, name2, value2, ..., NULL]. Walk into pairs. + def walk_attr_array(attrs_ptr) + return [] if attrs_ptr.null? + ptr_size = ::FFI.type_size(:pointer) + result = [] + offset = 0 + loop do + name_ptr = attrs_ptr.get_pointer(offset) + break if name_ptr.null? + value_ptr = attrs_ptr.get_pointer(offset + ptr_size) + break if value_ptr.null? + result << [name_ptr.read_string, value_ptr.read_string] + offset += 2 * ptr_size + end + result + end +end diff --git a/lib/taurus/xml/searchable.rb b/lib/taurus/xml/searchable.rb new file mode 100644 index 0000000..95c4434 --- /dev/null +++ b/lib/taurus/xml/searchable.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module Taurus::XML::Searchable + def xpath(*paths) + handler, _ns, _vars = parse_search_args(paths) + raise ArgumentError, "custom XPath handlers not supported" if handler + expr = paths.join(" | ") + + doc_ptr = is_a?(Taurus::XML::Document) ? c_ptr : document.c_ptr + context_ptr = is_a?(Taurus::XML::Document) ? nil : c_ptr + + result_ptr = Taurus::XML::FFI.taurus_xpath_eval(doc_ptr, context_ptr, expr) + if result_ptr.null? + raise Taurus::XML::XPathError, + Taurus::XML::FFI.taurus_status_string(Taurus::XML::FFI::TAURUS_ERROR_XPATH) + end + + wrap_xpath_result(result_ptr) + end + + def at_xpath(*paths) + result = xpath(*paths) + result.is_a?(Taurus::XML::NodeSet) ? result.first : result + end + + def search(*args) + paths = args.first.is_a?(Array) ? args.first : [args.first] + paths.map(&:to_s).all? { |p| looks_like_xpath?(p) } ? xpath(*paths) : css(*paths) + end + alias_method :/, :search + + def at(*args) + result = search(*args) + result.is_a?(Taurus::XML::NodeSet) ? result.first : result + end + alias_method :%, :at + + def css(*args) + handler, ns, _ = parse_search_args(args) + raise ArgumentError, "namespace bindings not supported in css" if ns && !ns.empty? + raise ArgumentError, "custom CSS handlers not supported" if handler + expr = args.map { |r| Taurus::XML::CssToXPath.convert(r) }.join(" | ") + xpath(expr) + end + + def at_css(*args) + result = css(*args) + result.is_a?(Taurus::XML::NodeSet) ? result.first : result + end + + protected + + def parse_search_args(args) + handler = args.find { |a| !a.is_a?(String) && !a.is_a?(Hash) && !a.is_a?(Symbol) } + args = args - [handler] if handler + hashes = [] + while args.last.is_a?(Hash) || args.last.nil? + hashes << args.pop + break if args.empty? + end + ns, vars = hashes.reverse + [handler, ns, vars] + end + + def looks_like_xpath?(str) + %r{\A(\./|/|\.\.|\.)}.match?(str) + end + + def wrap_xpath_result(result_ptr) + type = Taurus::XML::FFI.taurus_xpath_result_type(result_ptr) + case type + when Taurus::XML::FFI::XPATH_NODESET + Taurus::XML::NodeSet.send(:from_result, document, result_ptr) + when Taurus::XML::FFI::XPATH_BOOLEAN + v = Taurus::XML::FFI.taurus_xpath_result_boolean(result_ptr) != 0 + Taurus::XML::FFI.taurus_xpath_result_free(result_ptr) + v + when Taurus::XML::FFI::XPATH_NUMBER + v = Taurus::XML::FFI.taurus_xpath_result_number(result_ptr) + Taurus::XML::FFI.taurus_xpath_result_free(result_ptr) + v + when Taurus::XML::FFI::XPATH_STRING + str_ptr = Taurus::XML::FFI.taurus_xpath_result_string(result_ptr) + v = str_ptr.null? ? "" : str_ptr.read_string + Taurus::XML::FFI.taurus_free_string(str_ptr) unless str_ptr.null? + Taurus::XML::FFI.taurus_xpath_result_free(result_ptr) + v + else + Taurus::XML::FFI.taurus_xpath_result_free(result_ptr) + raise Taurus::XML::XPathError, "unknown xpath result type #{type}" + end + end +end diff --git a/lib/taurus/xml/text.rb b/lib/taurus/xml/text.rb new file mode 100644 index 0000000..c71a5b7 --- /dev/null +++ b/lib/taurus/xml/text.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Taurus::XML::Text < Taurus::XML::Node + def name; "text"; end + + def content + Taurus::XML::FFI.taurus_text_node_get_content(@c_ptr) + end + + def content=(new_content) + status = Taurus::XML::FFI.taurus_text_node_set_content(@c_ptr, new_content.to_s) + raise Taurus::XML::Error, + Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK + new_content + end +end diff --git a/lib/taurus/xpath.rb b/lib/taurus/xpath.rb deleted file mode 100644 index eb5f0d5..0000000 --- a/lib/taurus/xpath.rb +++ /dev/null @@ -1,152 +0,0 @@ -# frozen_string_literal: true - -module Taurus - # XPath 1.0 query engine providing full W3C specification compliance. - # - # The XPath module provides low-level access to the XPath engine's tokenizer, - # parser, and evaluator. For most use cases, you should use the high-level - # {Document#xpath} and {Element#xpath} methods instead. - # - # @example High-level usage (recommended) - # doc = Taurus.parse(xml) - # results = doc.xpath('//book[@price > 20]') - # - # @example Low-level tokenization - # tokens = Taurus::XPath.tokenize('//book') - # tokens # => [{type: :slash_slash, value: '//', ...}, ...] - # - # @example Low-level parsing - # ast = Taurus::XPath.parse('//book[1]') - # ast # => {type: :path, ...} - # - # @example Low-level evaluation - # result = Taurus::XPath.evaluate(doc, '//book', doc.root) - # - # == XPath 1.0 Features - # - # The engine implements the complete XPath 1.0 specification: - # - # === All 13 Axes - # - child, descendant, descendant-or-self - # - parent, ancestor, ancestor-or-self - # - self, following-sibling, preceding-sibling - # - following, preceding, attribute, namespace - # - # === All 27 Functions - # - String: string(), concat(), starts-with(), contains(), substring(), - # string-length(), normalize-space(), translate(), substring-before(), - # substring-after() - # - Boolean: boolean(), not(), true(), false(), lang() - # - Number: number(), sum(), floor(), ceiling(), round() - # - Node-set: count(), id(), last(), position(), local-name(), - # namespace-uri(), name() - # - # === All Operators - # - Logical: or, and - # - Equality: =, != - # - Relational: <, <=, >, >= - # - Arithmetic: +, -, *, div, mod - # - Union: | - # - # === Predicates - # - Position: [1], [N], [last()] - # - Boolean: [@attr], [element], [expression] - # - Comparison: [@price > 20], [@stock >= 5] - # - # @see Document#xpath - # @see Element#xpath - # @see https://www.w3.org/TR/1999/REC-xpath-19991116/ XPath 1.0 Specification - module XPath - class << self - # Tokenize an XPath expression into a stream of tokens - # - # Breaks down an XPath expression into its constituent tokens for parsing. - # This is the first stage of XPath processing. Most users should use - # {Document#xpath} instead. - # - # @param expression [String] the XPath expression to tokenize - # @return [Array] array of token hashes with :type, :value, :line, :column - # - # @example Tokenize a simple path - # tokens = Taurus::XPath.tokenize('//book') - # tokens[0] # => {type: :slash_slash, value: '//', line: 1, column: 0} - # tokens[1] # => {type: :name, value: 'book', line: 1, column: 2} - # - # @example Tokenize with predicates - # tokens = Taurus::XPath.tokenize('//book[@id="1"]') - # # Returns tokens for path, predicate, attribute, equality, string - # - # @see .parse - def tokenize(expression) - Taurus.xpath_tokenize(expression) - end - - # Parse an XPath expression into an Abstract Syntax Tree (AST) - # - # Converts a tokenized XPath expression into a tree structure representing - # its logical structure. This is the second stage of XPath processing. - # Most users should use {Document#xpath} instead. - # - # The AST is cached globally, so repeated calls with the same expression - # are extremely fast (O(1) hash lookup). - # - # @param expression [String] the XPath expression to parse - # @return [Hash] AST representation as a nested hash structure - # - # @example Parse a simple path - # ast = Taurus::XPath.parse('//book') - # ast[:type] # => :path - # - # @example Parse with functions - # ast = Taurus::XPath.parse('count(//book)') - # ast[:type] # => :function_call - # ast[:name] # => 'count' - # - # @see .tokenize - # @see .evaluate - def parse(expression) - Taurus.xpath_parse(expression) - end - - # Evaluate an XPath expression against a document - # - # Executes the XPath query and returns the result. This is the third and - # final stage of XPath processing. Most users should use {Document#xpath} - # or {Element#xpath} instead. - # - # @param document [Document] the document to query - # @param expression [String] the XPath expression to evaluate - # @param context_node [Element, Document, nil] the starting node for relative queries - # (default: document root) - # @return [Array, String, Float, Boolean] the evaluation result - # - Node-set queries return Array - # - String queries return String - # - Numeric queries return Float - # - Boolean queries return true or false - # - # @raise [ArgumentError] if expression is invalid - # - # @example Evaluate to node-set - # nodes = Taurus::XPath.evaluate(doc, '//book', doc.root) - # nodes # => [, , ...] - # - # @example Evaluate to number - # count = Taurus::XPath.evaluate(doc, 'count(//book)') - # count # => 2.0 - # - # @example Evaluate to boolean - # has_books = Taurus::XPath.evaluate(doc, 'boolean(//book)') - # has_books # => true - # - # @example Evaluate with context node - # item = doc.root.nodes.first - # children = Taurus::XPath.evaluate(doc, './child::*', item) - # - # @see Document#xpath - # @see Element#xpath - def evaluate(document, expression, context_node = nil) - Taurus.xpath_evaluate(document, expression, context_node) - end - end - end -end diff --git a/lib/taurus/xpath/ast/node.rb b/lib/taurus/xpath/ast/node.rb deleted file mode 100644 index 571719e..0000000 --- a/lib/taurus/xpath/ast/node.rb +++ /dev/null @@ -1,159 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module XPath - module AST - # Abstract base class for all XPath AST nodes - # - # All AST nodes must implement the #evaluate method which takes - # a context and returns a result (NodeSet, String, Number, or Boolean). - # - # @abstract Subclass and override {#evaluate} to implement - class Node - attr_reader :type, :children, :value - - # Initialize a new AST node - # - # @param type [Symbol] Node type - # @param children [Array] Child nodes - # @param value [Object] Optional value for leaf nodes - def initialize(type = :node, children = [], value = nil) - @type = type - @children = Array(children) - @value = value - end - - # Evaluate this AST node in the given context - # - # @param context [Taurus::XPath::Context] Evaluation context - # @return [Taurus::NodeSet, String, Numeric, Boolean] Result of evaluation - # @raise [NotImplementedError] if not overridden by subclass - def evaluate(context) - raise ::NotImplementedError, - "#{self.class}#evaluate must be implemented by subclass" - end - - # Check if this node is a constant value - # - # @return [Boolean] true if node represents a constant value - def constant? - false - end - - # Get the result type of this node - # - # @return [Symbol] One of :node_set, :string, :number, :boolean - def result_type - :unknown - end - - # String representation for debugging - # - # @return [String] Debug representation - def inspect - if @value - "#<#{self.class.name} @type=#{@type} @value=#{@value.inspect}>" - elsif @children.any? - "#<#{self.class.name} @type=#{@type} children=#{@children.size}>" - else - "#<#{self.class.name} @type=#{@type}>" - end - end - - alias to_s inspect - - # Factory methods for creating specific node types - - # Create an absolute path node (starts with / or //) - def self.absolute_path(descendant_or_self, *steps) - new(:absolute_path, [descendant_or_self] + steps) - end - - # Create a relative path node - def self.relative_path(*steps) - new(:relative_path, steps) - end - - # Create a path node - def self.path(*steps) - new(:path, steps) - end - - # Create an axis node - def self.axis(axis_name, node_test, *predicates) - new(:axis, [axis_name, node_test] + predicates) - end - - # Create a node test - def self.test(namespace, name) - new(:test, [], { namespace: namespace, name: name }) - end - - # Create a wildcard test - def self.wildcard - new(:wildcard) - end - - # Create a predicate node - def self.predicate(condition) - new(:predicate, [condition]) - end - - # Create a function call node - def self.function(name, *args) - new(:function, args, name) - end - - # Create a variable reference node - def self.variable(name) - new(:variable, [], name) - end - - # Create a literal string node - def self.string(value) - new(:string, [], value) - end - - # Create a literal number node - def self.number(value) - new(:number, [], value.to_f) - end - - # Create a binary operator node - def self.binary_op(operator, left, right) - new(:binary_op, [left, right], operator) - end - - # Create a unary operator node - def self.unary_op(operator, operand) - new(:unary_op, [operand], operator) - end - - # Create a union node (|) - def self.union(*expressions) - new(:union, expressions) - end - - # Create an attribute node - def self.attribute(name) - new(:attribute, [], name) - end - - # Create a current node (.) - def self.current - new(:current) - end - - # Create a parent node (..) - def self.parent - new(:parent) - end - - # Create a node type test (text(), comment(), etc.) - def self.node_type(type_name) - new(:node_type, [], type_name) - end - end - end - end -end diff --git a/lib/taurus/xpath/cache.rb b/lib/taurus/xpath/cache.rb deleted file mode 100644 index a338281..0000000 --- a/lib/taurus/xpath/cache.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module XPath - # Simple LRU (Least Recently Used) cache for compiled XPath expressions. - # - # @private - class Cache - DEFAULT_SIZE = 1000 - - # @param [Integer] max_size Maximum number of entries to cache - def initialize(max_size = DEFAULT_SIZE) - @max_size = max_size - @cache = {} - @access_order = [] - end - - # Gets a value from the cache or sets it using the provided block. - # - # @param [Object] key Cache key - # @yield Block to execute if key is not in cache - # @return [Object] Cached or newly computed value - def get_or_set(key) - if @cache.key?(key) - # Move to end (most recently used) - @access_order.delete(key) - @access_order.push(key) - @cache[key] - else - value = yield - set(key, value) - value - end - end - - # Sets a value in the cache. - # - # @param [Object] key - # @param [Object] value - # @return [Object] The value - def set(key, value) - if @cache.key?(key) - @access_order.delete(key) - elsif @cache.size >= @max_size - # Remove least recently used - lru_key = @access_order.shift - @cache.delete(lru_key) - end - - @cache[key] = value - @access_order.push(key) - value - end - - # Gets a value from the cache. - # - # @param [Object] key - # @return [Object, nil] - def get(key) - return unless @cache.key?(key) - - @access_order.delete(key) - @access_order.push(key) - @cache[key] - end - - # Clears the cache. - # - # @return [void] - def clear - @cache.clear - @access_order.clear - end - - # Returns the current size of the cache. - # - # @return [Integer] - def size - @cache.size - end - - # Checks if a key exists in the cache. - # - # @param [Object] key - # @return [Boolean] - def key?(key) - @cache.key?(key) - end - end - end -end diff --git a/lib/taurus/xpath/compiler.rb b/lib/taurus/xpath/compiler.rb deleted file mode 100644 index afc4bbc..0000000 --- a/lib/taurus/xpath/compiler.rb +++ /dev/null @@ -1,1768 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module XPath - # Compiler for transforming XPath AST into executable Ruby code. - # - # This class takes an XPath AST (produced by Parser) and compiles it into - # a Ruby Proc that can be executed against XML documents. The compilation - # process: - # - # 1. Traverse the XPath AST - # 2. Generate Ruby::Node AST representing Ruby code - # 3. Use Ruby::Generator to convert to Ruby source string - # 4. Evaluate source in Context to get a Proc - # - # @example - # ast = Parser.parse("//book") - # proc = Compiler.compile_with_cache(ast) - # result = proc.call(document) - # - # @private - class Compiler - # Shared context for compiled Procs - CONTEXT = Context.new - - # Expression cache - CACHE = Cache.new - - # Wildcard for node names/namespace prefixes - STAR = "*" - - # Node types that require a NodeSet to push nodes into - RETURN_NODESET = %i[path absolute_path relative_path axis - predicate].freeze - - # Compiles and caches an AST - # - # @param ast [AST::Node] XPath AST to compile - # @param namespaces [Hash, nil] Optional namespace prefix mappings - # @return [Proc] Compiled Proc that accepts a document - def self.compile_with_cache(ast, namespaces: nil) - cache_key = namespaces ? [ast, namespaces] : ast - CACHE.get_or_set(cache_key) { new(namespaces: namespaces).compile(ast) } - end - - # Initialize compiler - # - # @param namespaces [Hash, nil] Optional namespace prefix mappings - def initialize(namespaces: nil) - @namespaces = namespaces - @literal_id = 0 - @predicate_nodesets = [] - @predicate_indexes = [] - end - - # Compiles an XPath AST into a Ruby Proc - # - # @param ast [AST::Node] XPath AST to compile - # @return [Proc] Executable Proc - def compile(ast) - document = literal(:node) - matched = matched_literal - context_var = context_literal - - # Enable debug output - debug = ENV["DEBUG_XPATH"] == "1" - if debug - puts "\n#{'=' * 60}" - puts "COMPILING XPath" - puts "=" * 60 - puts "AST: #{ast.inspect}" - puts - end - - ruby_ast = if return_nodeset?(ast) - process(ast, document) { |node| matched.push(node) } - else - process(ast, document) - end - - proc_ast = literal(:lambda).add_block(document) do - # Get context from document - context_assign = context_var.assign(document.context) - - if return_nodeset?(ast) - # Create NodeSet using send node: Taurus::NodeSet.new([], context) - nodeset_class = const_ref("Moxml", "NodeSet") - empty_array = Ruby::Node.new(:array, []) - nodeset_new = Ruby::Node.new(:send, - [nodeset_class, "new", empty_array, - context_var]) - - body = matched.assign(nodeset_new) - .followed_by(ruby_ast) - .followed_by(matched) - else - body = ruby_ast - end - - context_assign.followed_by(body) - end - - generator = Ruby::Generator.new - source = generator.process(proc_ast) - - if debug - puts "GENERATED RUBY CODE:" - puts "-" * 60 - puts source - puts "=" * 60 - puts - end - - CONTEXT.evaluate(source) - ensure - @literal_id = 0 - @predicate_nodesets.clear - @predicate_indexes.clear - end - - # Process a single XPath AST node - # - # @param ast [AST::Node] AST node to process - # @param input [Ruby::Node] Input node - # @yield [Ruby::Node] Yields matched nodes if block given - # @return [Ruby::Node] Ruby AST node - def process(ast, input, &block) - send(:"on_#{ast.type}", ast, input, &block) - end - - # Dispatcher for generic binary operator nodes - def on_binary_op(ast, input, &block) - operator = ast.value # :eq, :lt, :add, :plus, :star, etc. - - # Map token names to handler method names - method_name = case operator - when :plus then :add - when :minus then :sub - when :star then :mul - else operator # eq, lt, gt, div, mod, etc. - end - - send(:"on_#{method_name}", ast, input, &block) - end - - # Dispatcher for generic unary operator nodes - def on_unary_op(ast, input, &block) - operator = ast.value # :minus - send(:"on_#{operator}", ast, input, &block) - end - - # Dispatcher for union nodes (parser creates :union, compiler uses :pipe) - def on_union(ast, input, &block) - on_pipe(ast, input, &block) - end - - private - - # Helper methods for creating Ruby AST nodes - - def literal(value) - case value - when Symbol, String - end - Ruby::Node.new(:lit, [value.to_s]) - end - - # Create a constant reference like Taurus::Document - def const_ref(*parts) - Ruby::Node.new(:const, parts) - end - - def unique_literal(name) - @literal_id += 1 - literal("#{name}#{@literal_id}") - end - - def string(value) - Ruby::Node.new(:string, [value.to_s]) - end - - def symbol(value) - Ruby::Node.new(:symbol, [value.to_sym]) - end - - def matched_literal - literal(:matched) - end - - def context_literal - literal(:context) - end - - def self_nil - @self_nil ||= literal(:nil) - end - - def self_true - @self_true ||= literal(true) - end - - def self_false - @self_false ||= literal(false) - end - - def return_nodeset?(ast) - # Special cases where relative_path returns node directly: - # - "." (current node) - # - ".." (parent node) - if ast.type == :relative_path && ast.children.size == 1 - child_type = ast.children[0].type - return false if %i[current parent].include?(child_type) - end - - RETURN_NODESET.include?(ast.type) - end - - # Type checking helpers - - def document_or_node(node) - doc_class = const_ref("Moxml", "Document") - node_class = const_ref("Moxml", "Node") - node.is_a?(doc_class).or(node.is_a?(node_class)) - end - - def element_or_attribute(node) - elem_class = const_ref("Moxml", "Element") - attr_class = const_ref("Moxml", "Attribute") - node.is_a?(elem_class).or(node.is_a?(attr_class)) - end - - def attribute_or_node(node) - attr_class = const_ref("Moxml", "Attribute") - node_class = const_ref("Moxml", "Node") - node.is_a?(attr_class).or(node.is_a?(node_class)) - end - - # Path handling - - # Handle absolute paths like /root or //descendant - def on_absolute_path(ast, input, &block) - if ast.children.empty? - # Just "/" - return the document/root - yield input if block - input - else - # Process steps from the input (which should be a document) - # Don't call input.root - that would skip a level - first_child = ast.children[0] - - # For absolute paths, we process from the document itself - if ast.children.size == 1 - process(first_child, input, &block) - else - # Multiple steps - create a path - path_node = AST::Node.new(:path, ast.children) - process(path_node, input, &block) - end - end - end - - # Handle relative paths - def on_relative_path(ast, input, &block) - on_path(ast, input, &block) - end - - # Handle path (series of steps) - def on_path(ast, input, &block) - return input if ast.children.empty? - - # First step from input - first_step = ast.children[0] - - if ast.children.size == 1 - # Single step - process(first_step, input, &block) - else - # Multiple steps - need to accumulate results - temp_results = unique_literal(:temp_results) - context_var = context_literal - - # Create NodeSet for temp results - nodeset_class = const_ref("Moxml", "NodeSet") - empty_array = Ruby::Node.new(:array, []) - nodeset_new = Ruby::Node.new(:send, - [nodeset_class, "new", empty_array, - context_var]) - - temp_results.assign(nodeset_new) - .followed_by do - process(first_step, input) do |node| - temp_results.push(node) - end - .followed_by do - # Process remaining steps on each result - remaining_steps = AST::Node.new(:path, ast.children[1..]) - temp_node = unique_literal(:temp_node) - - temp_results.each.add_block(temp_node) do - process(remaining_steps, temp_node, &block) - end - end - end - end - end - - # Axis handling - - # Dispatch axes to specific handlers - def on_axis(ast, input, &block) - axis_name, test, *_predicates = ast.children - - handler = axis_name.gsub("-", "_") - - send(:"on_axis_#{handler}", test, input, &block) - end - - # Handle step with predicates (created by parser) - def on_step_with_predicates(ast, input, &block) - step, *predicates = ast.children - - # If no predicates, just process the step - return process(step, input, &block) if predicates.empty? - - # Build predicate chain: step -> pred1 -> pred2 -> ... - # Each predicate wraps the previous result as its test - result_ast = step - - predicates.each do |pred_wrapper| - # pred_wrapper is :predicate node with children [expression] - # Build proper :predicate node with [test, expression, nil] - predicate_expr = pred_wrapper.children[0] - result_ast = AST::Node.new(:predicate, - [result_ast, predicate_expr, nil]) - end - - # Process the final chained AST - process(result_ast, input, &block) - end - - # AXIS: child - direct children - def on_axis_child(ast, input) - child = unique_literal(:child) - - document_or_node(input).if_true do - input.children.each.add_block(child) do - condition = process(ast, child) - if block_given? - condition.if_true { yield child } - else - condition.if_true { child } - end - end - end - end - - # AXIS: self - the node itself - def on_axis_self(ast, input) - condition = process(ast, input) - if block_given? - condition.if_true { yield input } - else - condition.if_true { input } - end - end - - # AXIS: parent - parent node - def on_axis_parent(ast, input) - parent = unique_literal(:parent) - - attribute_or_node(input).if_true do - parent.assign(input.parent).followed_by do - condition = process(ast, parent) - if block_given? - condition.if_true { yield parent } - else - condition.if_true { parent } - end - end - end - end - - # AXIS: descendant-or-self - Enables // operator - def on_axis_descendant_or_self(ast, input) - node = unique_literal(:descendant) - doc_class = const_ref("Moxml", "Document") - - document_or_node(input).if_true do - # Create a proper if-else structure that prevents double traversal - input.is_a?(doc_class).if_true do - # DOCUMENT PATH: test root, then traverse from root - root = unique_literal(:root) - root.assign(input.root).followed_by do - root.if_true do - # Test root first - condition = process(ast, root) - (if block_given? - condition.if_true { yield root } - else - condition.if_true { root } - end) - .followed_by do - # Traverse descendants FROM root only (not document.each_node) - root.each_node.add_block(node) do - desc_condition = process(ast, node) - if block_given? - desc_condition.if_true { yield node } - else - desc_condition.if_true { node } - end - end - end - end - end - end.else do - # NON-DOCUMENT PATH: test self, then traverse from self - condition = process(ast, input) - (if block_given? - condition.if_true { yield input } - else - condition.if_true { input } - end) - .followed_by do - # Traverse descendants FROM input - input.each_node.add_block(node) do - desc_condition = process(ast, node) - if block_given? - desc_condition.if_true { yield node } - else - desc_condition.if_true { node } - end - end - end - end - end - end - - # AXIS: attribute - Enables @attribute syntax - def on_axis_attribute(ast, input) - elem_class = const_ref("Moxml", "Element") - attribute = unique_literal(:attribute) - - input.is_a?(elem_class).if_true do - input.attributes.each.add_block(attribute) do - # Use process to handle both :test and :wildcard nodes - condition = process(ast, attribute) - - if block_given? - condition.if_true { yield attribute } - else - condition.if_true { attribute } - end - end - end - end - - # AXIS: descendant - All descendant nodes (without self) - def on_axis_descendant(ast, input) - node = unique_literal(:descendant) - - document_or_node(input).if_true do - input.each_node.add_block(node) do - condition = process(ast, node) - if block_given? - condition.if_true { yield node } - else - condition.if_true { node } - end - end - end - end - - # Helper: Recursively traverse all descendants - def traverse_all_descendants(input, &block) - child = unique_literal(:child) - - input.children.each.add_block(child) do - # Yield this child - yield child - # Then recursively traverse its descendants - traverse_all_descendants(child, &block) - end - end - - # Node test handling - - # Handle node tests (name matching) - def on_test(ast, input) - condition = element_or_attribute(input) - name_match = match_name_and_namespace(ast, input) - - name_match ? condition.and(name_match) : condition - end - - # Handle wildcard test (*) - def on_wildcard(_ast, input) - element_or_attribute(input) - end - - # Match element/attribute names and namespaces - def match_name_and_namespace(ast, input) - ns = ast.value[:namespace] - name = ast.value[:name] - - # Wildcard for both name and namespace means match all - return nil - # nil means "no additional constraint beyond type check" - return nil if name == STAR && (!ns || ns == STAR) - - condition = nil - name_str = string(name) - zero = literal(0) - - # Match name (case-insensitive) unless wildcard - if name != STAR - # If we have a namespace prefix, we need to compare local names - # For elements like "ns:item", we should compare against "item" not "ns:item" - if ns && ns != STAR && @namespaces && @namespaces[ns] - # Extract local name by splitting on ':' and taking the last part - # This handles both "ns:item" -> "item" and "item" -> "item" - local_name_expr = input.name.split(string(":")).last - condition = local_name_expr.eq(name_str) - .or(local_name_expr.casecmp(name_str).eq(zero)) - else - # No namespace or no mapping - compare full name - condition = input.name.eq(name_str) - .or(input.name.casecmp(name_str).eq(zero)) - end - end - - # Match namespace if specified - if ns && ns != STAR - if @namespaces && @namespaces[ns] - # Resolve prefix to URI using namespace mappings - ns_uri = @namespaces[ns] - ns_match = input.namespace.and(input.namespace.uri.eq(string(ns_uri))) - else - # No mapping provided - check against element's namespace prefix - # Need to ensure input.namespace exists first - ns_match = input.namespace.and(input.namespace.prefix.eq(string(ns))) - end - - condition = condition ? condition.and(ns_match) : ns_match - end - - condition - end - - # Literal value handling - - # String literals - def on_string(ast, *) - string(ast.value) - end - - # Number literals (both int and float) - def on_number(ast, *) - literal(ast.value.to_f.to_s) - end - - # Current node (.) - def on_current(_ast, input) - if block_given? - yield input # Block returns Ruby::Node for matched.push(input) - else - input - end - end - - # Parent node (..) - def on_parent(_ast, input) - input.parent - end - - # ===== OPERATORS ===== - - # Comparison: = (equality) - def on_eq(ast, input, &block) - conv = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - mass_assign([left, right], conv.to_compatible_types(left, right)) - .followed_by do - operation = left.eq(right) - - block ? operation.if_true(&block) : operation - end - end - end - - # Comparison: != (inequality) - def on_neq(ast, input, &block) - conv = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - mass_assign([left, right], conv.to_compatible_types(left, right)) - .followed_by do - operation = left != right - - block ? operation.if_true(&block) : operation - end - end - end - - # Comparison: < (less than) - def on_lt(ast, input, &block) - conversion = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - lval = conversion.to_float(left) - rval = conversion.to_float(right) - operation = lval < rval - - block ? conversion.to_boolean(operation).if_true(&block) : operation - end - end - - # Comparison: > (greater than) - def on_gt(ast, input, &block) - conversion = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - lval = conversion.to_float(left) - rval = conversion.to_float(right) - operation = lval > rval - - block ? conversion.to_boolean(operation).if_true(&block) : operation - end - end - - # Comparison: <= (less than or equal) - def on_lte(ast, input, &block) - conversion = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - lval = conversion.to_float(left) - rval = conversion.to_float(right) - operation = lval <= rval - - block ? conversion.to_boolean(operation).if_true(&block) : operation - end - end - - # Comparison: >= (greater than or equal) - def on_gte(ast, input, &block) - conversion = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - lval = conversion.to_float(left) - rval = conversion.to_float(right) - operation = lval >= rval - - block ? conversion.to_boolean(operation).if_true(&block) : operation - end - end - - # Arithmetic: + (addition) - def on_add(ast, input, &block) - conversion = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - lval = conversion.to_float(left) - rval = conversion.to_float(right) - operation = lval + rval - - block ? conversion.to_boolean(operation).if_true(&block) : operation - end - end - - # Arithmetic: - (subtraction) - def on_sub(ast, input, &block) - conversion = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - lval = conversion.to_float(left) - rval = conversion.to_float(right) - operation = lval - rval - - block ? conversion.to_boolean(operation).if_true(&block) : operation - end - end - - # Arithmetic: * (multiplication) - def on_mul(ast, input, &block) - conversion = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - lval = conversion.to_float(left) - rval = conversion.to_float(right) - operation = lval * rval - - block ? conversion.to_boolean(operation).if_true(&block) : operation - end - end - - # Arithmetic: div (division) - def on_div(ast, input, &block) - conversion = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - lval = conversion.to_float(left) - rval = conversion.to_float(right) - operation = lval / rval - - block ? conversion.to_boolean(operation).if_true(&block) : operation - end - end - - # Arithmetic: mod (modulo) - def on_mod(ast, input, &block) - conversion = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - lval = conversion.to_float(left) - rval = conversion.to_float(right) - operation = lval % rval - - block ? conversion.to_boolean(operation).if_true(&block) : operation - end - end - - # Unary: minus (negation) - def on_minus(ast, input, &block) - operand = ast.children[0] - operand_ast = process(operand, input) - conversion = literal(Taurus::XPath::Conversion) - - operand_var = unique_literal(:unary_operand) - operand_var.assign(operand_ast) - .followed_by do - negated = literal(0) - conversion.to_float(operand_var) - block ? conversion.to_boolean(negated).if_true(&block) : negated - end - end - - # Logical: and - def on_and(ast, input, &block) - conversion = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - lval = conversion.to_boolean(left) - rval = conversion.to_boolean(right) - operation = lval.and(rval) - - block ? conversion.to_boolean(operation).if_true(&block) : operation - end - end - - # Logical: or - def on_or(ast, input, &block) - conversion = literal(Taurus::XPath::Conversion) - - operator(ast, input) do |left, right| - lval = conversion.to_boolean(left) - rval = conversion.to_boolean(right) - operation = lval.or(rval) - - block ? conversion.to_boolean(operation).if_true(&block) : operation - end - end - - # Union: | (pipe) - def on_pipe(ast, input) - left, right = ast.children - - union = unique_literal(:union) - context_var = context_literal - - # Create NodeSet for union results - nodeset_class = const_ref("Moxml", "NodeSet") - empty_array = Ruby::Node.new(:array, []) - - # Expressions such as "a | b | c" - if left.type == :pipe - union.assign(process(left, input)) - .followed_by(process(right, input) { |node| union << node }) - .followed_by(union) - # Expressions such as "a | b" - else - nodeset_new = Ruby::Node.new(:send, - [nodeset_class, "new", empty_array, - context_var]) - - union.assign(nodeset_new) - .followed_by(process(left, input) { |node| union << node }) - .followed_by(process(right, input) { |node| union << node }) - .followed_by(union) - end - end - - # Variable: $variable - def on_var(ast, *) - name = ast.children[0] - - variables_literal.and(variables_literal[string(name)]) - .or(send_message(:raise, string("Undefined XPath variable: #{name}"))) - end - - # Predicate handling: //book[@price < 20] - def on_predicate(ast, input, &block) - test, predicate, following = ast.children - - index_var = unique_literal(:index) - - # Check predicate type to determine strategy - method = if number?(predicate) - :on_predicate_index - elsif has_call_node?(predicate, "last") - :on_predicate_temporary - else - :on_predicate_direct - end - - @predicate_indexes << index_var - - result = index_var.assign(literal(1)).followed_by do - send(method, input, test, predicate) do |matched| - if following - process(following, matched, &block) - else - yield matched - end - end - end - - @predicate_indexes.pop - - result - end - - # Predicate that requires temporary NodeSet (for last()) - def on_predicate_temporary(input, test, predicate) - temp_set = unique_literal(:temp_set) - pred_node = unique_literal(:pred_node) - pred_var = unique_literal(:pred_var) - conversion = literal(Taurus::XPath::Conversion) - context_var = context_literal - - index_var = predicate_index - index_step = literal(1) - - @predicate_nodesets << temp_set - - # Create NodeSet for temp results - nodeset_class = const_ref("Moxml", "NodeSet") - empty_array = Ruby::Node.new(:array, []) - nodeset_new = Ruby::Node.new(:send, - [nodeset_class, "new", empty_array, - context_var]) - - ast = temp_set.assign(nodeset_new) - .followed_by do - process(test, input) { |node| temp_set << node } - end - .followed_by do - temp_set.each.add_block(pred_node) do - pred_ast = process(predicate, pred_node) - - pred_var.assign(pred_ast) - .followed_by do - pred_var.is_a?(literal(:Numeric)).if_true do - pred_var.assign(pred_var.to_i.eq(index_var)) - end - end - .followed_by do - conversion.to_boolean(pred_var).if_true { yield pred_node } - end - .followed_by do - index_var.assign(index_var + index_step) - end - end - end - - @predicate_nodesets.pop - - ast - end - - # Predicate that doesn't require temporary NodeSet - def on_predicate_direct(input, test, predicate) - pred_var = unique_literal(:pred_var) - index_var = predicate_index - index_step = literal(1) - conversion = literal(Taurus::XPath::Conversion) - - process(test, input) do |matched_test_node| - pred_ast = if return_nodeset?(predicate) - # Use catch/throw for early return - catch_message(:predicate_matched) do - process(predicate, matched_test_node) do - throw_message(:predicate_matched, self_true) - end - end - else - process(predicate, matched_test_node) - end - - pred_var.assign(pred_ast) - .followed_by do - pred_var.is_a?(literal(:Numeric)).if_true do - pred_var.assign(pred_var.to_i.eq(index_var)) - end - end - .followed_by do - conversion.to_boolean(pred_var).if_true do - yield matched_test_node - end - end - .followed_by do - index_var.assign(index_var + index_step) - end - end - end - - # Predicate with literal index: //book[1] - def on_predicate_index(input, test, predicate) - index_var = predicate_index - index_step = literal(1) - - index = process(predicate, input).to_i - - process(test, input) do |matched_test_node| - index_var.eq(index) - .if_true do - yield matched_test_node - end - .followed_by do - index_var.assign(index_var + index_step) - end - end - end - - # ===== XPATH FUNCTIONS ===== - - # XPath function dispatcher - def on_call(ast, input, &block) - # Function name is stored in value field, not children - name = ast.value - args = ast.children - - handler = name.to_s.gsub("-", "_") - - send(:"on_call_#{handler}", input, *args, &block) - end - - # Alias for function nodes (parser creates :function, compiler uses on_call) - alias on_function on_call - - # 1. string() - Convert value to string - def on_call_string(input, arg = nil) - convert_var = unique_literal(:convert) - conversion = literal(Taurus::XPath::Conversion) - - argument_or_first_node(input, arg) do |arg_var| - convert_var.assign(conversion.to_string(arg_var)) - .followed_by do - if block_given? - convert_var.empty?.if_false { yield convert_var } - else - convert_var - end - end - end - end - - # 2. concat() - Concatenate strings - def on_call_concat(input, *args) - conversion = literal(Taurus::XPath::Conversion) - assigns = [] - conversions = [] - - args.each do |arg| - arg_var = unique_literal(:concat_arg) - arg_ast = try_match_first_node(arg, input) - - assigns << arg_var.assign(arg_ast) - conversions << conversion.to_string(arg_var) - end - - concatted = assigns.inject(:followed_by) - .followed_by(conversions.inject(:+)) - - block_given? ? concatted.empty?.if_false { yield concatted } : concatted - end - - # 3. starts-with() - Check string prefix - def on_call_starts_with(input, haystack, needle) - haystack_var = unique_literal(:haystack) - needle_var = unique_literal(:needle) - conversion = literal(Taurus::XPath::Conversion) - - haystack_var.assign(try_match_first_node(haystack, input)) - .followed_by do - needle_var.assign(try_match_first_node(needle, input)) - end - .followed_by do - haystack_var.assign(conversion.to_string(haystack_var)) - .followed_by do - needle_var.assign(conversion.to_string(needle_var)) - end - .followed_by do - equal = needle_var.empty? - .or(haystack_var.start_with?(needle_var)) - - block_given? ? equal.if_true { yield equal } : equal - end - end - end - - # 4. contains() - Check substring - def on_call_contains(input, haystack, needle) - haystack_lit = unique_literal(:haystack) - needle_lit = unique_literal(:needle) - conversion = literal(Taurus::XPath::Conversion) - - haystack_lit.assign(try_match_first_node(haystack, input)) - .followed_by do - needle_lit.assign(try_match_first_node(needle, input)) - end - .followed_by do - converted = conversion.to_string(haystack_lit) - .include?(conversion.to_string(needle_lit)) - - block_given? ? converted.if_true { yield converted } : converted - end - end - - # 5. substring-before() - Get part before separator - def on_call_substring_before(input, haystack, needle) - haystack_var = unique_literal(:haystack) - needle_var = unique_literal(:needle) - conversion = literal(Taurus::XPath::Conversion) - - before = unique_literal(:before) - sep = unique_literal(:sep) - after = unique_literal(:after) - - haystack_var.assign(try_match_first_node(haystack, input)) - .followed_by do - needle_var.assign(try_match_first_node(needle, input)) - end - .followed_by do - converted = conversion.to_string(haystack_var) - .partition(conversion.to_string(needle_var)) - - mass_assign([before, sep, after], converted).followed_by do - sep.empty? - .if_true { sep } - .else { block_given? ? yield : before } - end - end - end - - # 6. substring-after() - Get part after separator - def on_call_substring_after(input, haystack, needle) - haystack_var = unique_literal(:haystack) - needle_var = unique_literal(:needle) - conversion = literal(Taurus::XPath::Conversion) - - before = unique_literal(:before) - sep = unique_literal(:sep) - after = unique_literal(:after) - - haystack_var.assign(try_match_first_node(haystack, input)) - .followed_by do - needle_var.assign(try_match_first_node(needle, input)) - end - .followed_by do - converted = conversion.to_string(haystack_var) - .partition(conversion.to_string(needle_var)) - - mass_assign([before, sep, after], converted).followed_by do - sep.empty? - .if_true { sep } - .else { block_given? ? yield : after } - end - end - end - - # 7. substring() - Extract substring - def on_call_substring(input, haystack, start, length = nil) - haystack_var = unique_literal(:haystack) - start_var = unique_literal(:start) - length_var = unique_literal(:length) - result_var = unique_literal(:result) - ruby_start = unique_literal(:ruby_start) - effective_length = unique_literal(:effective_length) - conversion = literal(Taurus::XPath::Conversion) - - haystack_var.assign(try_match_first_node(haystack, input)) - .followed_by do - haystack_var.assign(conversion.to_string(haystack_var)) - end - .followed_by do - start_var.assign(try_match_first_node(start, input)) - .followed_by do - # Round the start position first (XPath 1.0 spec requires rounding) - start_var.assign(conversion.to_float(start_var).round.to_i) - end - end - .followed_by do - if length - length_var.assign(try_match_first_node(length, input)) - .followed_by do - # Round the length (XPath 1.0 spec requires rounding) - length_var.assign(conversion.to_float(length_var).round.to_i) - end - .followed_by do - # XPath 1.0 algorithm: - # If start < 1, some positions fall before the string - # We need to adjust the effective length accordingly - # effective_length = (start + length) - max(start, 1) - # lua_start = max(start, 1) - 1 (since we start from position 1) - - # Calculate how many positions to skip before position 1 - # If start is 0, we lose 1 position; if -2, we lose 3 positions - ruby_start.assign( - (start_var < literal(1)) - .if_true { literal(0) } - .else { start_var - literal(1) }, - ) - end - .followed_by do - # Calculate effective length accounting for positions before string - effective_length.assign( - (start_var < literal(1)) - .if_true do - # Some positions are before position 1 - # end_pos = start + length - # effective = end_pos - 1 (since we start from position 1) - # But clamp to 0 if entirely before string - ((start_var + length_var) - literal(1)) - .if_true { (start_var + length_var) - literal(1) } - .else { literal(0) } - end - .else { length_var }, - ) - end - .followed_by do - # Clamp effective length to non-negative - effective_length.assign( - (effective_length < literal(0)) - .if_true { literal(0) } - .else { effective_length }, - ) - end - .followed_by do - # Extract substring with effective length - result_var.assign(haystack_var[ruby_start, effective_length]) - .followed_by do - # Ensure we return empty string instead of nil - result_var.assign(result_var.if_true do - result_var - end.else { string("") }) - end - end - .followed_by do - if block_given? - result_var.empty?.if_false do - yield result_var - end - else - result_var - end - end - else - # No length specified - go to end of string - # Convert to 0-based index, clamping to 0 - ruby_start.assign( - (start_var < literal(1)) - .if_true { literal(0) } - .else { start_var - literal(1) }, - ).followed_by do - # Extract from start to end - result_var.assign(haystack_var[range(ruby_start, literal(-1))]) - .followed_by do - # Ensure we return empty string instead of nil - result_var.assign(result_var.if_true do - result_var - end.else { string("") }) - end - end - .followed_by do - if block_given? - result_var.empty?.if_false do - yield result_var - end - else - result_var - end - end - end - end - end - - # 8. string-length() - Get string length - def on_call_string_length(input, arg = nil) - convert_var = unique_literal(:convert) - conversion = literal(Taurus::XPath::Conversion) - - argument_or_first_node(input, arg) do |arg_var| - convert_var.assign(conversion.to_string(arg_var).length) - .followed_by do - if block_given? - convert_var.zero?.if_false { yield convert_var } - else - convert_var.to_f - end - end - end - end - - # 9. normalize-space() - Normalize whitespace - def on_call_normalize_space(input, arg = nil) - conversion = literal(Taurus::XPath::Conversion) - norm_var = unique_literal(:normalized) - - # Create regex for matching whitespace sequences - # Use Regexp.new to create /\s+/ pattern at runtime - regexp_class = const_ref("Regexp") - whitespace_pattern = string('\\s+') - whitespace_regex = Ruby::Node.new(:send, - [regexp_class, "new", - whitespace_pattern]) - replace = string(" ") - - argument_or_first_node(input, arg) do |arg_var| - norm_var - .assign(conversion.to_string(arg_var).strip.gsub(whitespace_regex, - replace)) - .followed_by do - norm_var.empty? - .if_true { string("") } - .else { block_given? ? yield : norm_var } - end - end - end - - # 10. translate() - Character replacement - def on_call_translate(input, source, find, replace) - source_var = unique_literal(:source) - find_var = unique_literal(:find) - replace_var = unique_literal(:replace) - replaced_var = unique_literal(:replaced) - conversion = literal(Taurus::XPath::Conversion) - - char = unique_literal(:char) - index = unique_literal(:index) - - source_var.assign(try_match_first_node(source, input)) - .followed_by do - replaced_var.assign(conversion.to_string(source_var)) - end - .followed_by do - find_var.assign(try_match_first_node(find, input)) - end - .followed_by do - find_var.assign(conversion.to_string(find_var).chars.to_array) - end - .followed_by do - replace_var.assign(try_match_first_node(replace, input)) - end - .followed_by do - replace_var.assign(conversion.to_string(replace_var).chars.to_array) - end - .followed_by do - find_var.each_with_index.add_block(char, index) do - replace_with = replace_var[index] - .if_true { replace_var[index] } - .else { string("") } - - replaced_var.assign(replaced_var.gsub(char, replace_with)) - end - end - .followed_by { replaced_var } - end - - # ===== NUMERIC FUNCTIONS ===== - - # 1. number() - Convert to number - def on_call_number(input, arg = nil, &block) - convert_var = unique_literal(:convert) - conversion = literal(Taurus::XPath::Conversion) - - argument_or_first_node(input, arg) do |arg_var| - convert_var.assign(conversion.to_float(arg_var)).followed_by do - if block - convert_var.zero?.if_false(&block) - else - convert_var - end - end - end - end - - # 2. sum() - Sum node values - def on_call_sum(input, arg, &block) - unless return_nodeset?(arg) - raise TypeError, "sum() can only operate on a path, axis or predicate" - end - - sum_var = unique_literal(:sum) - conversion = literal(Taurus::XPath::Conversion) - - sum_var.assign(literal(0.0)) - .followed_by do - process(arg, input) do |matched_node| - sum_var.assign(sum_var + conversion.to_float(matched_node.text)) - end - end - .followed_by do - block ? sum_var.zero?.if_false(&block) : sum_var - end - end - - # 3. count() - Count nodes - def on_call_count(input, arg, &block) - count = unique_literal(:count) - - unless return_nodeset?(arg) - raise TypeError, "count() can only operate on NodeSet instances" - end - - count.assign(literal(0.0)) - .followed_by do - process(arg, input) { count.assign(count + literal(1)) } - end - .followed_by do - block ? count.zero?.if_false(&block) : count - end - end - - # 4. floor() - Round down - def on_call_floor(input, arg) - arg_ast = try_match_first_node(arg, input) - call_arg = unique_literal(:call_arg) - conversion = literal(Taurus::XPath::Conversion) - - call_arg.assign(arg_ast) - .followed_by do - call_arg.assign(conversion.to_float(call_arg)) - end - .followed_by do - call_arg.nan? - .if_true { call_arg } - .else { block_given? ? yield : call_arg.floor.to_f } - end - end - - # 5. ceiling() - Round up - def on_call_ceiling(input, arg) - arg_ast = try_match_first_node(arg, input) - call_arg = unique_literal(:call_arg) - conversion = literal(Taurus::XPath::Conversion) - - call_arg.assign(arg_ast) - .followed_by do - call_arg.assign(conversion.to_float(call_arg)) - end - .followed_by do - call_arg.nan? - .if_true { call_arg } - .else { block_given? ? yield : call_arg.ceil.to_f } - end - end - - # 6. round() - Round to nearest - def on_call_round(input, arg) - arg_ast = try_match_first_node(arg, input) - call_arg = unique_literal(:call_arg) - conversion = literal(Taurus::XPath::Conversion) - - call_arg.assign(arg_ast) - .followed_by do - call_arg.assign(conversion.to_float(call_arg)) - end - .followed_by do - call_arg.nan? - .if_true { call_arg } - .else { block_given? ? yield : call_arg.round.to_f } - end - end - - # ===== BOOLEAN FUNCTIONS ===== - - # 1. boolean() - Convert to boolean - def on_call_boolean(input, arg, &block) - arg_ast = try_match_first_node(arg, input) - call_arg = unique_literal(:call_arg) - conversion = literal(Taurus::XPath::Conversion) - - call_arg.assign(arg_ast).followed_by do - converted = conversion.to_boolean(call_arg) - - block ? converted.if_true(&block) : converted - end - end - - # 2. not() - Negate boolean - def on_call_not(input, arg, &block) - arg_ast = try_match_first_node(arg, input) - call_arg = unique_literal(:call_arg) - conversion = literal(Taurus::XPath::Conversion) - - call_arg.assign(arg_ast).followed_by do - converted = conversion.to_boolean(call_arg).not - - block ? converted.if_true(&block) : converted - end - end - - # 3. true() - Return true - def on_call_true(*) - block_given? ? yield : self_true - end - - # 4. false() - Return false - def on_call_false(*) - self_false - end - - # ===== NODE FUNCTIONS ===== - - # 1. local-name() - Get local name without namespace prefix - def on_call_local_name(input, arg = nil) - argument_or_first_node(input, arg) do |arg_var| - arg_var - .if_true do - ensure_element_or_attribute(arg_var) - .followed_by { block_given? ? yield : arg_var.name } - end - .else { string("") } - end - end - - # 2. name() - Get expanded/qualified name with namespace - def on_call_name(input, arg = nil) - argument_or_first_node(input, arg) do |arg_var| - arg_var - .if_true do - ensure_element_or_attribute(arg_var) - .followed_by { block_given? ? yield : arg_var.expanded_name } - end - .else { string("") } - end - end - - # 3. namespace-uri() - Get namespace URI - def on_call_namespace_uri(input, arg = nil) - default = string("") - - argument_or_first_node(input, arg) do |arg_var| - arg_var - .if_true do - ensure_element_or_attribute(arg_var).followed_by do - arg_var.namespace - .if_true { block_given? ? yield : arg_var.namespace.uri } - .else { default } - end - end - .else { default } - end - end - - # 4. lang() - Check xml:lang attribute - def on_call_lang(input, arg) - lang_var = unique_literal("lang") - node = unique_literal("node") - found = unique_literal("found") - xml_lang = unique_literal("xml_lang") - matched = unique_literal("matched") - - conversion = literal(Taurus::XPath::Conversion) - - ast = lang_var.assign(try_match_first_node(arg, input)) - .followed_by do - lang_var.assign(conversion.to_string(lang_var)) - end - .followed_by do - matched.assign(self_false) - end - .followed_by do - node.assign(input) - end - .followed_by do - xml_lang.assign(string("xml:lang")) - end - .followed_by do - node.respond_to?(symbol(:attribute)).while_true do - found.assign(node.get(xml_lang)) - .followed_by do - found.if_true do - found.eq(lang_var) - .if_true do - if block_given? - yield - else - matched.assign(self_true).followed_by(break_loop) - end - end - .else { break_loop } - end - end - .followed_by(node.assign(node.parent)) - end - end - - block_given? ? ast : ast.followed_by(matched) - end - - # ===== POSITION FUNCTIONS ===== - - # 1. position() - Current position in predicate context - def on_call_position(*) - index = predicate_index - - unless index - raise InvalidContextError.new( - "position() requires a predicate context. " \ - "Use position() within a predicate like: //item[position() = 1]", - function_name: "position()", - required_context: "predicate", - ) - end - - index.to_f - end - - # 2. last() - Size of current predicate context - def on_call_last(*) - set = predicate_nodeset - - unless set - raise InvalidContextError.new( - "last() requires a predicate context. " \ - "Use last() within a predicate like: //item[position() = last()]", - function_name: "last()", - required_context: "predicate", - ) - end - - set.length.to_f - end - - # ===== SPECIAL FUNCTIONS ===== - - # 1. id() - Find nodes by ID attribute - def on_call_id(input, arg) - orig_input = original_input_literal - node = unique_literal(:node) - ids_var = unique_literal("ids") - matched = unique_literal("id_matched") - id_str_var = unique_literal("id_string") - attr_var = unique_literal("attr") - - nodeset_class = const_ref("Moxml", "NodeSet") - context_var = context_literal - empty_array = Ruby::Node.new(:array, []) - - matched.assign(Ruby::Node.new(:send, - [nodeset_class, "new", empty_array, - context_var])) - .followed_by do - # When using a path, get text of all matched nodes - if return_nodeset?(arg) - empty_ids = Ruby::Node.new(:array, []) - ids_var.assign(empty_ids).followed_by do - process(arg, input) { |element| ids_var << element.text } - end - # Otherwise cast to string and split on spaces - else - conversion = literal(Taurus::XPath::Conversion) - ids_var.assign(process(arg, input)) - .followed_by do - ids_var.assign(conversion.to_string(ids_var).split(string(" "))) - end - end - end - .followed_by do - id_str_var.assign(string("id")) - end - .followed_by do - orig_input.each_node.add_block(node) do - node.is_a?(const_ref("Moxml", "Element")).if_true do - attr_var.assign(node.attribute(id_str_var)).followed_by do - attr_var.and(ids_var.include?(attr_var.value)) - .if_true { block_given? ? yield : matched << node } - end - end - end - end - .followed_by(matched) - end - - # Helper methods - - # Helper: Get argument or use current node's first child - def argument_or_first_node(input, arg = nil) - arg_ast = arg ? try_match_first_node(arg, input) : input - arg_var = unique_literal(:argument_or_first_node) - - arg_var.assign(arg_ast).followed_by { yield arg_var } - end - - # Helper: Try to match first node v1 - def try_match_first_node_v1(ast, input, optimize_first = true) - if return_nodeset?(ast) && optimize_first - matched_set = unique_literal(:matched_set) - first_node = unique_literal(:first_node) - context_var = context_literal - - # Create NodeSet for results - nodeset_class = const_ref("Moxml", "NodeSet") - empty_array = Ruby::Node.new(:array, []) - nodeset_new = Ruby::Node.new(:send, - [nodeset_class, "new", empty_array, - context_var]) - - matched_set.assign(nodeset_new) - .followed_by do - # Process with block to accumulate results - process(ast, input) { |node| matched_set.push(node) } - end - .followed_by do - first_node.assign(matched_set[literal(0)]) - end - .followed_by do - first_node.if_true { first_node }.else { string("") } - end - else - process(ast, input) - end - end - - # Helper: Create mass assignment node - def mass_assign(vars, value) - Ruby::Node.new(:massign, [vars, value]) - end - - # Helper: Create range node for Ruby AST - def range(start, stop) - Ruby::Node.new(:range, [start, stop]) - end - - # Helper: Ensure node is Element or Attribute - def ensure_element_or_attribute(input) - element_or_attribute(input).if_false do - raise_message(TypeError, "argument is not an Element or Attribute") - end - end - - # Helper: Raise an error with message - def raise_message(klass, message) - send_message(:raise, literal(klass), string(message)) - end - - # Helper: Send a message (for method calls like raise, break) - def send_message(name, *args) - Ruby::Node.new(:send, [nil, name.to_s] + args) - end - - # Helper: Break statement - def break_loop - send_message(:break) - end - - # Helper: Get current predicate index - def predicate_index - @predicate_indexes.last - end - - # Helper: Get current predicate nodeset - def predicate_nodeset - @predicate_nodesets.last - end - - # Helper: Get original input literal for traversal - def original_input_literal - literal(:node) - end - - # Helper: Generate code for an operator - # - # Processes left and right operands, optimizing to match only first node - # when appropriate (path, axis, predicate) - def operator(ast, input, optimize_first = true) - left, right = ast.children - - left_var = unique_literal(:op_left) - right_var = unique_literal(:op_right) - - left_ast = try_match_first_node(left, input, optimize_first) - right_ast = try_match_first_node(right, input, optimize_first) - - left_var.assign(left_ast) - .followed_by(right_var.assign(right_ast)) - .followed_by { yield left_var, right_var } - end - - # Helper: Try to match first node in a set, otherwise process as usual - def try_match_first_node(ast, input, optimize_first = true) - if return_nodeset?(ast) && optimize_first - matched_set = unique_literal(:matched_set) - first_node = unique_literal(:first_node) - context_var = context_literal - - # Create NodeSet for results - nodeset_class = const_ref("Moxml", "NodeSet") - empty_array = Ruby::Node.new(:array, []) - nodeset_new = Ruby::Node.new(:send, - [nodeset_class, "new", empty_array, - context_var]) - - matched_set.assign(nodeset_new) - .followed_by do - # Process with block to accumulate results - process(ast, input) { |node| matched_set.push(node) } - end - .followed_by do - first_node.assign(matched_set[literal(0)]) - end - .followed_by { first_node } - else - process(ast, input) - end - end - - # Helper: Check if AST node is a number - def number?(ast) - %i[int float number].include?(ast.type) - end - - # Helper: Check if AST contains a call node with given name - def has_call_node?(ast, name) - visit = [ast] - - until visit.empty? - current = visit.pop - - return true if current.type == :call && current.children[0] == name - - current.children.each do |child| - visit << child if child.is_a?(AST::Node) - end - end - - false - end - - # Helper: Catch a message (for early returns) - def catch_message(name) - send_message(:catch, symbol(name)).add_block do - # Ensure catch only returns value when throw is invoked - yield.followed_by(self_nil) - end - end - - # Helper: Throw a message with optional arguments - def throw_message(name, *args) - send_message(:throw, symbol(name), *args) - end - - # Helper: Variables literal for variable support - def variables_literal - literal(:variables) - end - end - end -end diff --git a/lib/taurus/xpath/context.rb b/lib/taurus/xpath/context.rb deleted file mode 100644 index 8d25ba2..0000000 --- a/lib/taurus/xpath/context.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module XPath - # Class used as the context for compiled XPath Procs. - # - # The binding of this class is used for the binding of Procs compiled by - # {Compiler}. Not using a specific binding would result in the procs using - # the binding of {Compiler#compile}, which could lead to race conditions. - # - # @private - class Context - def initialize - @binding = binding - end - - # Evaluates a Ruby code string in this context's binding. - # - # @param [String] string Ruby code to evaluate - # @return [Proc] - def evaluate(string) - @binding.eval(string) - end - end - end -end diff --git a/lib/taurus/xpath/conversion.rb b/lib/taurus/xpath/conversion.rb deleted file mode 100644 index 3730977..0000000 --- a/lib/taurus/xpath/conversion.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module XPath - # Module for converting XPath objects such as NodeSets to different types. - # - # @private - module Conversion - # Converts both arguments to a type that can be compared using ==. - # - # @param [Object] left - # @param [Object] right - # @return [Array] - def self.to_compatible_types(left, right) - if left.is_a?(Taurus::NodeSet) || left.respond_to?(:text) - left = to_string(left) - end - - if right.is_a?(Taurus::NodeSet) || right.respond_to?(:text) - right = to_string(right) - end - - if left.is_a?(Numeric) && !right.is_a?(Numeric) - right = to_float(right) - end - - if left.is_a?(String) && !right.is_a?(String) - right = to_string(right) - end - - if boolean?(left) && !boolean?(right) - right = to_boolean(right) - end - - [left, right] - end - - # Converts a value to an XPath string. - # - # @param [Object] value - # @return [String] - def self.to_string(value) - # If we have a number that has a zero decimal (e.g. 10.0) we want to - # get rid of that decimal. For this we'll first convert the number to - # an integer. - if value.is_a?(Float) && value.modulo(1).zero? - value = value.to_i - end - - if value.is_a?(Taurus::NodeSet) - value = first_node_text(value) - end - - if value.respond_to?(:text) - value = value.text - end - - value.to_s - end - - # Converts a value to an XPath number (float). - # - # @param [Object] value - # @return [Float] - def self.to_float(value) - if value.is_a?(Taurus::NodeSet) - value = first_node_text(value) - end - - if value.respond_to?(:text) - value = value.text - end - - if value == true - 1.0 - elsif value == false - 0.0 - else - begin - Float(value) - rescue ArgumentError, TypeError - Float::NAN - end - end - end - - # Converts a value to an XPath boolean. - # - # @param [Object] value - # @return [Boolean] - def self.to_boolean(value) - bool = false - - if value.is_a?(Float) - bool = !value.nan? && !value.zero? - elsif value.is_a?(Integer) - bool = !value.zero? - elsif value.respond_to?(:empty?) - bool = !value.empty? - elsif value - bool = true - end - - bool - end - - # Checks if a value is a boolean. - # - # @param [Object] value - # @return [Boolean] - def self.boolean?(value) - [true, false].include?(value) - end - - # Gets the text of the first node in a NodeSet. - # - # @param [Taurus::NodeSet] set - # @return [String] - def self.first_node_text(set) - set[0].respond_to?(:text) ? set[0].text : "" - end - end - end -end diff --git a/lib/taurus/xpath/engine.rb b/lib/taurus/xpath/engine.rb deleted file mode 100644 index f476431..0000000 --- a/lib/taurus/xpath/engine.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module XPath - # XPath 1.0 evaluation engine - # - # This engine provides complete XPath 1.0 support for Moxml documents, - # particularly useful for the Ox adapter which has limited native XPath. - # - # @example Evaluate XPath expression - # engine = Taurus::XPath::Engine.new(document) - # results = engine.evaluate("//book[@id='123']/title") - # - # @example With context node - # engine = Taurus::XPath::Engine.new(document) - # results = engine.evaluate("./author", context: book_element) - # - class Engine - attr_reader :document - - # Initialize engine with a document - # - # @param document [Taurus::Document] The document to query - def initialize(document) - @document = document - end - - # Evaluate an XPath expression - # - # @param expression [String] XPath expression to evaluate - # @param context [Taurus::Node, nil] Context node (defaults to document root) - # @return [Taurus::NodeSet, String, Numeric, Boolean] Result depends on expression - # @raise [Taurus::XPath::SyntaxError] If expression syntax is invalid - # @raise [Taurus::XPath::EvaluationError] If evaluation fails - def evaluate(expression, context: nil) - # TEMPORARY: Skip C parsing entirely for now - context_node = context || document.root - Taurus::NodeSet.new([context_node]) - rescue => e - raise Taurus::XPath::EvaluationError, "XPath evaluation failed: #{e.message}" - end - - # Check if expression is valid XPath syntax - # - # @param expression [String] XPath expression to validate - # @return [Boolean] true if valid, false otherwise - def valid?(expression) - evaluate(expression, context: document.root) - true - rescue Taurus::XPath::SyntaxError - false - end - end - end -end diff --git a/lib/taurus/xpath/errors.rb b/lib/taurus/xpath/errors.rb deleted file mode 100644 index 910f354..0000000 --- a/lib/taurus/xpath/errors.rb +++ /dev/null @@ -1,116 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module XPath - # Base error for XPath-specific errors - class Error < StandardError - attr_reader :expression - - def initialize(message, expression: nil) - @expression = expression - super(message) - end - - def to_s - msg = super - msg += "\n Expression: #{@expression}" if @expression - msg - end - end - - # Error raised when XPath syntax is invalid - class SyntaxError < Error - attr_reader :position, :token - - def initialize(message, expression: nil, position: nil, token: nil) - @position = position - @token = token - super(message, expression: expression) - end - - def to_s - msg = super - msg += "\n Position: #{@position}" if @position - msg += "\n Unexpected token: #{@token.inspect}" if @token - msg - end - end - - # Error raised when XPath evaluation fails - class EvaluationError < Error - attr_reader :context_node, :step - - def initialize(message, expression: nil, context_node: nil, step: nil) - @context_node = context_node - @step = step - super(message, expression: expression) - end - - def to_s - msg = super - msg += "\n Context node: <#{@context_node.name}>" if @context_node.respond_to?(:name) - msg += "\n Step: #{@step}" if @step - msg - end - end - - # Error raised when an XPath function is not found or invalid - class FunctionError < Error - attr_reader :function_name, :argument_count - - def initialize(message, expression: nil, function_name: nil, -argument_count: nil) - @function_name = function_name - @argument_count = argument_count - super(message, expression: expression) - end - - def to_s - msg = super - msg += "\n Function: #{@function_name}" if @function_name - msg += "\n Arguments: #{@argument_count}" if @argument_count - msg - end - end - - # Error raised when an XPath operation on unsupported node type - class NodeTypeError < Error - attr_reader :node_type, :operation - - def initialize(message, expression: nil, node_type: nil, operation: nil) - @node_type = node_type - @operation = operation - super(message, expression: expression) - end - - def to_s - msg = super - msg += "\n Node type: #{@node_type}" if @node_type - msg += "\n Operation: #{@operation}" if @operation - msg - end - end - - # Error raised when an XPath function is called without required context - class InvalidContextError < Error - attr_reader :function_name, :required_context - - def initialize(message, expression: nil, function_name: nil, -required_context: nil) - @function_name = function_name - @required_context = required_context - super(message, expression: expression) - end - - def to_s - msg = super - msg += "\n Function: #{@function_name}" if @function_name - msg += "\n Required context: #{@required_context}" if @required_context - msg - end - end - end - - # Alias for backward compatibility - XPathError = XPath::Error -end diff --git a/lib/taurus/xpath/lexer.rb b/lib/taurus/xpath/lexer.rb deleted file mode 100644 index 1407531..0000000 --- a/lib/taurus/xpath/lexer.rb +++ /dev/null @@ -1,304 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module XPath - # XPath expression lexer/tokenizer - # - # Converts XPath expressions into a stream of tokens for parsing. - # Each token is represented as [type, value, position]. - # - # @example - # lexer = Lexer.new("//book[@id='123']") - # tokens = lexer.tokenize - # # => [[:dslash, "//", 0], [:name, "book", 2], ...] - class Lexer - # XPath axis names for recognition - AXIS_NAMES = %w[ - ancestor ancestor-or-self attribute child descendant - descendant-or-self following following-sibling namespace - parent preceding preceding-sibling self - ].freeze - - # XPath node type names - NODE_TYPES = %w[ - comment text processing-instruction node - ].freeze - - # Reserved keywords - KEYWORDS = %w[and or mod div].freeze - - # Initialize lexer with XPath expression - # - # @param expression [String] XPath expression to tokenize - def initialize(expression) - @expression = expression.to_s - @position = 0 - @length = @expression.length - @tokens = [] - end - - # Tokenize the XPath expression - # - # @return [Array] Array of [type, value, position] tuples - # @raise [XPath::SyntaxError] if expression contains invalid syntax - def tokenize - @tokens = [] - @position = 0 - - while @position < @length - skip_whitespace - break if @position >= @length - - token_start = @position - - case current_char - when "/" - if peek_char == "/" - add_token(:dslash, "//", token_start) - advance(2) - else - add_token(:slash, "/", token_start) - advance - end - when "|" - add_token(:pipe, "|", token_start) - advance - when "+" - add_token(:plus, "+", token_start) - advance - when "-" - add_token(:minus, "-", token_start) - advance - when "*" - add_token(:star, "*", token_start) - advance - when "=" - add_token(:eq, "=", token_start) - advance - when "!" - if peek_char == "=" - add_token(:neq, "!=", token_start) - advance(2) - else - raise_syntax_error("Unexpected '!' at position #{@position}") - end - when "<" - if peek_char == "=" - add_token(:lte, "<=", token_start) - advance(2) - else - add_token(:lt, "<", token_start) - advance - end - when ">" - if peek_char == "=" - add_token(:gte, ">=", token_start) - advance(2) - else - add_token(:gt, ">", token_start) - advance - end - when "(" - add_token(:lparen, "(", token_start) - advance - when ")" - add_token(:rparen, ")", token_start) - advance - when "[" - add_token(:lbracket, "[", token_start) - advance - when "]" - add_token(:rbracket, "]", token_start) - advance - when "," - add_token(:comma, ",", token_start) - advance - when "@" - add_token(:at, "@", token_start) - advance - when ":" - if peek_char == ":" - add_token(:dcolon, "::", token_start) - advance(2) - else - add_token(:colon, ":", token_start) - advance - end - when "." - if peek_char == "." - add_token(:ddot, "..", token_start) - advance(2) - elsif /\d/.match?(peek_char) - scan_number(token_start) - else - add_token(:dot, ".", token_start) - advance - end - when "$" - add_token(:dollar, "$", token_start) - advance - when '"', "'" - scan_string(token_start) - when /\d/ - scan_number(token_start) - when /[a-zA-Z_]/ - scan_name_or_keyword(token_start) - else - raise_syntax_error( - "Unexpected character '#{current_char}' at position #{@position}", - ) - end - end - - @tokens - end - - private - - # Get current character - # - # @return [String, nil] Current character or nil if at end - def current_char - @expression[@position] - end - - # Peek at next character - # - # @return [String, nil] Next character or nil if at end - def peek_char - @expression[@position + 1] - end - - # Advance position by n characters - # - # @param n [Integer] Number of characters to advance - def advance(n = 1) - @position += n - end - - # Skip whitespace characters - def skip_whitespace - @position += 1 while @position < @length && - @expression[@position] =~ /\s/ - end - - # Add token to token list - # - # @param type [Symbol] Token type - # @param value [String] Token value - # @param position [Integer] Token position - def add_token(type, value, position) - @tokens << [type, value, position] - end - - # Scan string literal - # - # @param start_pos [Integer] Starting position - def scan_string(start_pos) - quote = current_char - advance - - value = "" - while @position < @length && current_char != quote - if current_char == "\\" - advance - if @position < @length - # Handle escape sequences - value += case current_char - when "t" - "\t" - when "n" - "\n" - when "r" - "\r" - when "\\" - "\\" - when '"' - '"' - when "'" - "'" - else - # Unknown escape - add literally - current_char - end - end - else - value += current_char - end - advance - end - - if @position >= @length - raise_syntax_error("Unterminated string starting at position #{start_pos}") - end - - advance # Skip closing quote - add_token(:string, value, start_pos) - end - - # Scan number (integer or decimal) - # - # @param start_pos [Integer] Starting position - def scan_number(start_pos) - value = "" - - # Integer part - while @position < @length && current_char =~ /\d/ - value += current_char - advance - end - - # Decimal part - if @position < @length && current_char == "." - value += current_char - advance - - while @position < @length && current_char =~ /\d/ - value += current_char - advance - end - end - - add_token(:number, value, start_pos) - end - - # Scan name or keyword - # - # @param start_pos [Integer] Starting position - def scan_name_or_keyword(start_pos) - value = "" - - # Name can contain letters, digits, underscores, hyphens, and dots - while @position < @length && current_char =~ /[a-zA-Z0-9_\-.]/ - value += current_char - advance - end - - # Check if it's an axis name followed by :: - if AXIS_NAMES.include?(value) && - @position < @length - 1 && - @expression[@position, 2] == "::" - add_token(:axis, value, start_pos) - elsif KEYWORDS.include?(value) - add_token(value.to_sym, value, start_pos) - elsif NODE_TYPES.include?(value) - add_token(:node_type, value, start_pos) - else - add_token(:name, value, start_pos) - end - end - - # Raise syntax error - # - # @param message [String] Error message - # @raise [XPath::SyntaxError] - def raise_syntax_error(message) - raise Taurus::XPath::SyntaxError.new( - message, - expression: @expression, - position: @position, - ) - end - end - end -end diff --git a/lib/taurus/xpath/parser.rb b/lib/taurus/xpath/parser.rb deleted file mode 100644 index 6eb83f4..0000000 --- a/lib/taurus/xpath/parser.rb +++ /dev/null @@ -1,485 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module XPath - # XPath expression parser - # - # Implements a recursive descent parser for XPath 1.0 expressions. - # Builds an Abstract Syntax Tree (AST) from tokenized input. - # - # Grammar (simplified XPath 1.0): - # expr ::= or_expr - # or_expr ::= and_expr ('or' and_expr)* - # and_expr ::= equality ('and' equality)* - # equality ::= relational (('=' | '!=') relational)* - # relational ::= additive (('<' | '>' | '<=' | '>=') additive)* - # additive ::= multiplicative (('+' | '-') multiplicative)* - # multiplicative ::= unary (('*' | 'div' | 'mod') unary)* - # unary ::= ('-')? union - # union ::= path_expr ('|' path_expr)* - # path_expr ::= filter_expr | location_path - # filter_expr ::= primary_expr predicate* - # primary ::= variable | '(' expr ')' | literal | number | function - # location_path ::= absolute_path | relative_path - # - # @example - # ast = Parser.parse("//book[@id='123']") - # ast = Parser.parse_with_cache("//book[@id='123']") - class Parser - # Parse cache for compiled expressions - CACHE = Cache.new(100) - - # Parse an XPath expression - # - # @param expression [String] XPath expression to parse - # @return [AST::Node] Root node of AST - # @raise [XPath::SyntaxError] if expression is invalid - def self.parse(expression) - new(expression).parse - end - - # Parse with caching - # - # @param expression [String] XPath expression to parse - # @return [AST::Node] Root node of AST (possibly cached) - def self.parse_with_cache(expression) - CACHE.get_or_set(expression) { parse(expression) } - end - - # Initialize parser with expression - # - # @param expression [String] XPath expression - def initialize(expression) - @expression = expression.to_s - @lexer = Lexer.new(@expression) - @tokens = @lexer.tokenize - @position = 0 - end - - # Parse the expression into an AST - # - # @return [AST::Node] Root node of AST - # @raise [XPath::SyntaxError] if expression is invalid - def parse - return AST::Node.new(:empty) if @tokens.empty? - - result = parse_expr - - unless at_end? - raise_syntax_error("Unexpected token after expression: #{current_token}") - end - - result - end - - private - - # Get current token - # - # @return [Array, nil] Current token [type, value, position] - def current_token - @tokens[@position] - end - - # Get current token type - # - # @return [Symbol, nil] Token type - def current_type - current_token&.first - end - - # Get current token value - # - # @return [String, nil] Token value - def current_value - current_token&.[](1) - end - - # Check if at end of tokens - # - # @return [Boolean] - def at_end? - @position >= @tokens.length - end - - # Advance to next token - # - # @return [Array, nil] Previous token - def advance - token = current_token - @position += 1 - token - end - - # Check if current token matches type - # - # @param types [Array] Token types to check - # @return [Boolean] - def match?(*types) - types.any?(current_type) - end - - # Consume token if it matches, otherwise error - # - # @param type [Symbol] Expected token type - # @param message [String] Error message if not found - # @return [Array] Consumed token - # @raise [XPath::SyntaxError] if token doesn't match - def consume(type, message) - if current_type == type - advance - else - raise_syntax_error(message) - end - end - - # Raise syntax error - # - # @param message [String] Error message - # @raise [XPath::SyntaxError] - def raise_syntax_error(message) - position = current_token&.[](2) || @expression.length - raise XPath::SyntaxError.new( - message, - expression: @expression, - position: position, - ) - end - - # Parse top-level expression - def parse_expr - parse_or_expr - end - - # Parse OR expression - def parse_or_expr - left = parse_and_expr - - while match?(:or) - advance - right = parse_and_expr - left = AST::Node.binary_op(:or, left, right) - end - - left - end - - # Parse AND expression - def parse_and_expr - left = parse_equality - - while match?(:and) - advance - right = parse_equality - left = AST::Node.binary_op(:and, left, right) - end - - left - end - - # Parse equality expression - def parse_equality - left = parse_relational - - while match?(:eq, :neq) - op = current_type - advance - right = parse_relational - left = AST::Node.binary_op(op, left, right) - end - - left - end - - # Parse relational expression - def parse_relational - left = parse_additive - - while match?(:lt, :gt, :lte, :gte) - op = current_type - advance - right = parse_additive - left = AST::Node.binary_op(op, left, right) - end - - left - end - - # Parse additive expression - def parse_additive - left = parse_multiplicative - - while match?(:plus, :minus) - op = current_type - advance - right = parse_multiplicative - left = AST::Node.binary_op(op, left, right) - end - - left - end - - # Parse multiplicative expression - def parse_multiplicative - left = parse_unary - - while match?(:star, :div, :mod) - op = current_type - advance - right = parse_unary - left = AST::Node.binary_op(op, left, right) - end - - left - end - - # Parse unary expression - def parse_unary - if match?(:minus) - advance - operand = parse_union - return AST::Node.unary_op(:minus, operand) - end - - parse_union - end - - # Parse union expression - def parse_union - left = parse_path_expr - - if match?(:pipe) - paths = [left] - while match?(:pipe) - advance - paths << parse_path_expr - end - return AST::Node.union(*paths) - end - - left - end - - # Parse path expression (location path or filter expression) - def parse_path_expr - # Check for absolute path - if match?(:slash, :dslash) - return parse_location_path - end - - # Check for primary expression (could be filter expression) - if match?(:string, :number, :dollar, :lparen) || - (match?(:name) && peek_is?(:lparen)) - # Primary expression that could be filtered - expr = parse_primary - - # Check for predicates (filter expression) - if match?(:lbracket) - predicates = [] - while match?(:lbracket) - advance - condition = parse_expr - consume(:rbracket, "Expected ']' after predicate") - predicates << AST::Node.predicate(condition) - end - expr = AST::Node.new(:filter_expr, [expr] + predicates) - end - - return expr - end - - # Otherwise, it's a location path - parse_location_path - end - - # Check if next token matches type - def peek_is?(type) - @tokens[@position + 1]&.first == type - end - - # Parse location path - def parse_location_path - if match?(:slash) - advance - # Absolute path: / - if at_end? || match?(:pipe, :rbracket, :rparen, :comma) - return AST::Node.absolute_path(AST::Node.current) - end - - # Absolute path with steps: /step1/step2 - steps = parse_relative_path - return AST::Node.absolute_path(*steps.children) - elsif match?(:dslash) - advance - # Descendant-or-self: // - steps = parse_relative_path - return AST::Node.absolute_path( - AST::Node.axis("descendant-or-self", AST::Node.wildcard), - *steps.children, - ) - end - - # Relative path - parse_relative_path - end - - # Parse relative path (series of steps) - def parse_relative_path - steps = [parse_step] - - while match?(:slash) && !at_end? - advance - if match?(:slash) - # Double slash within path - advance - steps << AST::Node.axis("descendant-or-self", AST::Node.wildcard) - end - steps << parse_step unless at_end? || match?(:pipe, :rbracket, - :rparen, :comma) - end - - AST::Node.relative_path(*steps) - end - - # Parse a single step - def parse_step - # Abbreviated steps - if match?(:dot) - advance - return AST::Node.current - elsif match?(:ddot) - advance - return AST::Node.parent - elsif match?(:at) - advance - # Attribute: @name - name = consume(:name, "Expected attribute name after @") - node_test = AST::Node.test(nil, name[1]) - step = AST::Node.axis("attribute", node_test) - return parse_predicates(step) - end - - # Full axis step or abbreviated child step - if match?(:axis) - axis_name = current_value - advance - consume(:dcolon, "Expected '::' after axis name") - node_test = parse_node_test - step = AST::Node.axis(axis_name, node_test) - else - # Abbreviated child axis - node_test = parse_node_test - step = AST::Node.axis("child", node_test) - end - - parse_predicates(step) - end - - # Parse node test - def parse_node_test - if match?(:star) - advance - return AST::Node.wildcard - elsif match?(:node_type) - type_name = current_value - advance - consume(:lparen, "Expected '(' after node type") - consume(:rparen, "Expected ')' after node type") - return AST::Node.node_type(type_name) - elsif match?(:name, :and, :or, :mod, :div) - # Accept keywords as valid element names (they're valid XML names) - name = current_value - advance - - # Check for namespace prefix - if match?(:colon) && !match?(:dcolon) - advance - if match?(:star) - advance - return AST::Node.test(name, "*") - elsif match?(:name, :and, :or, :mod, :div) - # Accept keywords as local names too - local_name = current_value - advance - return AST::Node.test(name, local_name) - else - raise_syntax_error("Expected local name after namespace") - end - end - - return AST::Node.test(nil, name) - end - - raise_syntax_error("Expected node test") - end - - # Parse predicates - def parse_predicates(step) - predicates = [] - - while match?(:lbracket) - advance - condition = parse_expr - consume(:rbracket, "Expected ']' after predicate") - predicates << AST::Node.predicate(condition) - end - - return step if predicates.empty? - - # Attach predicates to step - AST::Node.new(:step_with_predicates, [step] + predicates) - end - - # Parse primary expression - def parse_primary - if match?(:string) - value = current_value - advance - return AST::Node.string(value) - elsif match?(:number) - value = current_value - advance - # Convert string to actual numeric value - numeric_value = value.include?(".") ? value.to_f : value.to_i - return AST::Node.number(numeric_value) - elsif match?(:dollar) - advance - name = consume(:name, "Expected variable name after $") - return AST::Node.variable(name[1]) - elsif match?(:lparen) - advance - expr = parse_expr - consume(:rparen, "Expected ')' after expression") - return expr - elsif match?(:name) - name = current_value - advance - - # Check for function call - if match?(:lparen) - advance - args = [] - - unless match?(:rparen) - args << parse_expr - while match?(:comma) - advance - args << parse_expr - end - end - - consume(:rparen, "Expected ')' after function arguments") - return AST::Node.function(name, *args) - end - - # Just a name without function call - shouldn't happen in parse_primary - # but return it as a relative path - @position -= 1 # Put the name back - return parse_location_path - end - - raise_syntax_error("Expected primary expression") - end - end - end -end diff --git a/lib/taurus/xpath/ruby/generator.rb b/lib/taurus/xpath/ruby/generator.rb deleted file mode 100644 index 62b1f32..0000000 --- a/lib/taurus/xpath/ruby/generator.rb +++ /dev/null @@ -1,269 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module XPath - module Ruby - # Class for converting a Ruby AST to a String. - # - # This class takes a {Taurus::XPath::Ruby::Node} instance and converts it - # (and its child nodes) to a String that can be passed to `eval`. - # - # @private - class Generator - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def process(ast) - handler = :"on_#{ast.type}" - unless respond_to?(handler, true) - raise NotImplementedError, - "Generator missing handler for node type :#{ast.type}. Node: #{ast.inspect}" - end - - send(handler, ast) - end - - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_followed_by(ast) - ast.to_a.map { |child| process(child) }.join("\n\n") - end - - # Processes an assignment node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_assign(ast) - var, val = *ast - - var_str = process(var) - val_str = process(val) - - "#{var_str} = #{val_str}" - end - - # Processes a mass assignment node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_massign(ast) - vars, val = *ast - - var_names = vars.map { |var| process(var) } - val_str = process(val) - - "#{var_names.join(', ')} = #{val_str}" - end - - # Processes a `begin` node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_begin(ast) - body = process(ast.to_a[0]) - - <<~RUBY - begin - #{body} - end - RUBY - end - - # Processes an equality node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_eq(ast) - left, right = *ast - - left_str = process(left) - right_str = process(right) - - "#{left_str} == #{right_str}" - end - - # Processes a boolean "and" node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_and(ast) - left, right = *ast - - left_str = process(left) - right_str = process(right) - - "#{left_str} && #{right_str}" - end - - # Processes a boolean "or" node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_or(ast) - left, right = *ast - - left_str = process(left) - right_str = process(right) - - "(#{left_str} || #{right_str})" - end - - # Processes an if statement node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_if(ast) - cond, body, else_body = *ast - - cond_str = process(cond) - body_str = process(body) - - if else_body - else_str = process(else_body) - - <<~RUBY - if #{cond_str} - #{body_str} - else - #{else_str} - end - RUBY - else - <<~RUBY - if #{cond_str} - #{body_str} - end - RUBY - end - end - - # Processes a while statement node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_while(ast) - cond, body = *ast - - cond_str = process(cond) - body_str = process(body) - - <<~RUBY - while #{cond_str} - #{body_str} - end - RUBY - end - - # Processes a method call node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_send(ast) - children = ast.to_a - receiver = children[0] - name = children[1] - args = children[2..] || [] - - call = name - brackets = name == "[]" - - unless args.empty? - arg_strs = [] - args.each do |arg| - result = process(arg) - # Keep processing if we got a Node back (happens with nested send nodes) - while result.respond_to?(:type) - result = process(result) - end - arg_strs << result - end - arg_str = arg_strs.join(", ") - call = brackets ? "[#{arg_str}]" : "#{call}(#{arg_str})" - end - - if receiver - rec_str = process(receiver) - # Keep processing if we got a Node back - while rec_str.respond_to?(:type) - rec_str = process(rec_str) - end - call = brackets ? "#{rec_str}#{call}" : "#{rec_str}.#{call}" - end - - call - end - - # Processes a block node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_block(ast) - receiver, args, body = *ast - - receiver_str = process(receiver) - body_str = body ? process(body) : nil - arg_strs = args.map { |arg| process(arg) } - - <<~RUBY - #{receiver_str} do |#{arg_strs.join(', ')}| - #{body_str} - end - RUBY - end - - # Processes a Range node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_range(ast) - start, stop = *ast - - start_str = process(start) - stop_str = process(stop) - - "(#{start_str}..#{stop_str})" - end - - # Processes a string node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_string(ast) - ast.to_a[0].inspect - end - - # Processes a Symbol node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_symbol(ast) - ast.to_a[0].to_sym.inspect - end - - # Processes a literal node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_lit(ast) - ast.to_a[0] - end - - # Processes a constant reference node (e.g., Taurus::Document). - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_const(ast) - ast.to_a.join("::") - end - - # Processes an array literal node. - # - # @param [Taurus::XPath::Ruby::Node] ast - # @return [String] - def on_array(ast) - elements = ast.to_a.map { |elem| process(elem) } - "[#{elements.join(', ')}]" - end - end - end - end -end diff --git a/lib/taurus/xpath/ruby/node.rb b/lib/taurus/xpath/ruby/node.rb deleted file mode 100644 index d881b3a..0000000 --- a/lib/taurus/xpath/ruby/node.rb +++ /dev/null @@ -1,193 +0,0 @@ -# frozen_string_literal: true - -module Taurus - module XPath - module Ruby - # Class representing a single node in a Ruby AST. - # - # This class provides a fluent DSL for building Ruby code dynamically. - # It's modeled after the "ast" gem but simplified to avoid method conflicts. - # - # @example Building an if statement - # number1 = Node.new(:lit, ['10']) - # number2 = Node.new(:lit, ['20']) - # - # (number2 > number1).if_true do - # Node.new(:lit, ['30']) - # end - # - # @private - class Node < BasicObject - undef_method :!, :!= - - # @return [Symbol] - attr_reader :type - - # @param [Symbol] type The type of AST node - # @param [Array] children Child nodes or values - def initialize(type, children = []) - @type = type.to_sym - @children = children - end - - # @return [Array] - def to_a - @children - end - alias to_ary to_a - - # Returns a "to_a" call node. - # - # @return [Taurus::XPath::Ruby::Node] - def to_array - Node.new(:send, [self, :to_a]) - end - - # Returns an assignment node. - # - # Wraps assigned values in a begin/end block to ensure that - # multiple lines of code result in the proper value being assigned. - # - # @param [Taurus::XPath::Ruby::Node] other - # @return [Taurus::XPath::Ruby::Node] - def assign(other) - other = other.wrap if other.type == :followed_by - - Node.new(:assign, [self, other]) - end - - # Returns an equality expression node. - # - # @param [Taurus::XPath::Ruby::Node] other - # @return [Taurus::XPath::Ruby::Node] - def eq(other) - Node.new(:eq, [self, other]) - end - - # Returns a boolean "and" node. - # - # @param [Taurus::XPath::Ruby::Node] other - # @return [Taurus::XPath::Ruby::Node] - def and(other) - Node.new(:and, [self, other]) - end - - # Returns a boolean "or" node. - # - # @param [Taurus::XPath::Ruby::Node] other - # @return [Taurus::XPath::Ruby::Node] - def or(other) - Node.new(:or, [self, other]) - end - - # Returns a node that evaluates to its inverse. - # - # @example - # foo.not # => !foo - # - # @return [Taurus::XPath::Ruby::Node] - def not - !self - end - - # Returns a node for Ruby's "is_a?" method. - # - # @param [Class] klass - # @return [Taurus::XPath::Ruby::Node] - def is_a?(klass) - # If klass is already a Node (e.g., a const node), use it directly - # Otherwise wrap it in a lit node - klass_node = if klass.respond_to?(:type) - klass - else - Node.new(:lit, [klass.to_s]) - end - - Node.new(:send, [self, "is_a?", klass_node]) - end - - # Wraps the current node in a block. - # - # @param [Array] args Arguments (as Node instances) to pass to the block - # @return [Taurus::XPath::Ruby::Node] - def add_block(*args) - Node.new(:block, [self, args, yield]) - end - - # Wraps the current node in a `begin` node. - # - # @return [Taurus::XPath::Ruby::Node] - def wrap - Node.new(:begin, [self]) - end - - # Wraps the current node in an if statement node. - # - # The body of this statement is set to the return value of the supplied - # block. - # - # @return [Taurus::XPath::Ruby::Node] - def if_true - Node.new(:if, [self, yield]) - end - - # Wraps the current node in an `if !...` statement. - # - # @see [#if_true] - def if_false(&block) - self.not.if_true(&block) - end - - # Wraps the current node in a `while` statement. - # - # The body of this statement is set to the return value of the supplied - # block. - # - # @return [Taurus::XPath::Ruby::Node] - def while_true - Node.new(:while, [self, yield]) - end - - # Adds an "else" statement to the current node. - # - # This method assumes it's being called only on "if" nodes. - # - # @return [Taurus::XPath::Ruby::Node] - def else - Node.new(:if, @children + [yield]) - end - - # Chains two nodes together. - # - # @param [Taurus::XPath::Ruby::Node] other - # @return [Taurus::XPath::Ruby::Node] - def followed_by(other = nil) - other = yield if ::Kernel.block_given? - - Node.new(:followed_by, [self, other]) - end - - # Returns a node for a method call. - # - # @param [Symbol] name The name of the method to call - # @param [Array] args Any arguments (as Node instances) to pass - # @return [Taurus::XPath::Ruby::Node] - def method_missing(name, *args) - Node.new(:send, [self, name.to_s, *args]) - end - - # Prevent implicit string conversion - Nodes must be explicitly processed - def to_str - ::Kernel.raise ::TypeError, "Cannot implicitly - - convert #{self.class} to String. Use Generator#process instead." - end - - # @return [String] - def inspect - "(#{type} #{@children.map(&:inspect).join(' ')})" - end - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9ed7b4d..4ffe70a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,18 +1,10 @@ # frozen_string_literal: true -require "taurus" -require "moxml" - -# Load support files -Dir[File.expand_path("support/**/*.rb", __dir__)].each { |f| require f } +require "taurus/xml" RSpec.configure do |config| - # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" - - # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! - config.expect_with :rspec do |c| c.syntax = :expect end diff --git a/spec/support/xpath_helpers.rb b/spec/support/xpath_helpers.rb deleted file mode 100644 index ca83fa0..0000000 --- a/spec/support/xpath_helpers.rb +++ /dev/null @@ -1,120 +0,0 @@ -# frozen_string_literal: true - -module XPathHelpers - # Parse XML string into a Taurus document - def parse(xml) - Taurus.parse(xml) - end - - # Execute XPath query on a node - def xpath(node, expression) - node.xpath(expression) - end - - # Verify XPath result with comprehensive checks - def expect_xpath_result(node, expression, expected_count: nil, expected_names: nil, expected_values: nil, expected_types: nil) - result = node.xpath(expression) - - expect(result).to be_an(Array), "XPath result should be an Array" - - if expected_count - expect(result.size).to eq(expected_count), - "Expected #{expected_count} results, got #{result.size}" - end - - if expected_names - names = result.map { |e| e.respond_to?(:name) ? e.name : nil }.compact - expect(names).to eq(expected_names), - "Expected names #{expected_names.inspect}, got #{names.inspect}" - end - - if expected_values - values = result.map do |item| - if item.is_a?(String) - item - elsif item.respond_to?(:text) - item.text - else - item.to_s - end - end - expect(values).to eq(expected_values), - "Expected values #{expected_values.inspect}, got #{values.inspect}" - end - - if expected_types - types = result.map(&:class) - expect(types).to eq(expected_types), - "Expected types #{expected_types.inspect}, got #{types.inspect}" - end - - result - end - - # Quick assertion for XPath result count - def assert_xpath_count(node, expression, count) - result = node.xpath(expression) - expect(result.size).to eq(count), - "XPath '#{expression}' expected #{count} results, got #{result.size}" - end - - # Quick assertion for XPath result element names - def assert_xpath_names(node, expression, *names) - result = node.xpath(expression) - actual = result.map { |e| e.respond_to?(:name) ? e.name : nil }.compact - expect(actual).to eq(names), - "XPath '#{expression}' expected names #{names.inspect}, got #{actual.inspect}" - end - - # Quick assertion for XPath result returning specific element - def assert_xpath_element(node, expression, expected_name) - result = node.xpath(expression) - expect(result).not_to be_empty, "XPath '#{expression}' returned no results" - expect(result.first).to be_a(Taurus::Element) - expect(result.first.name).to eq(expected_name) - end - - # Quick assertion for XPath returning empty result - def assert_xpath_empty(node, expression) - result = node.xpath(expression) - expect(result).to be_empty, - "XPath '#{expression}' expected empty result, got #{result.size} items" - end - - # Helper to create a simple XML document for testing - def create_test_document(xml_string = nil) - xml_string ||= <<~XML - - First - Second - - XML - parse(xml_string) - end - - # Helper to navigate to a specific element by path - def navigate_to(root, path) - parts = path.split('/') - current = root - - parts.each do |part| - next if part.empty? - current = current.nodes.find { |n| n.is_a?(Taurus::Element) && n.name == part } - break if current.nil? - end - - current - end - - # Helper to verify element hierarchy - def verify_parent_child(parent, child) - expect(child.parent).to eq(parent), - "Expected #{child.name} to have #{parent.name} as parent" - expect(parent.nodes).to include(child), - "Expected #{parent.name} to contain #{child.name} in nodes" - end -end - -RSpec.configure do |config| - config.include XPathHelpers -end \ No newline at end of file diff --git a/spec/taurus/adapter/taurus_spec.rb b/spec/taurus/adapter/taurus_spec.rb deleted file mode 100644 index b3185c6..0000000 --- a/spec/taurus/adapter/taurus_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe Taurus::Adapter::Taurus do - let(:adapter) { described_class } - let(:xml) do - <<~XML - - - Book 1 - Author A - - - Book 2 - Author B - - - XML - end - - describe ".parse" do - it "parses XML using Taurus" do - doc = adapter.parse(xml) - - expect(doc).to be_a(Taurus::Document) - expect(doc.root.name).to eq("root") - end - end - - describe ".xpath" do - let(:doc) { adapter.parse(xml) } - - it "executes simple XPath queries" do - # Taurus has complete XPath 1.0 support through C extension - result = adapter.xpath(doc.root, "book") - - expect(result).to be_a(Taurus::NodeSet) - expect(result.size).to eq(2) - end - end - - describe ".at_xpath" do - let(:doc) { adapter.parse(xml) } - - it "returns first matching node" do - result = adapter.at_xpath(doc.root, "book") - - expect(result).to be_a(Taurus::Element) - expect(result.name).to eq("book") - end - end - - describe ".xpath_supported?" do - it "returns true" do - expect(adapter.xpath_supported?).to be true - end - end - - describe ".capabilities" do - it "reports basic capabilities" do - caps = adapter.capabilities - - expect(caps[:parse]).to be true - expect(caps[:namespace_aware]).to be true - expect(caps[:xpath_support]).to eq(:full) # Full XPath 1.0! - expect(caps[:xpath_full]).to be true - expect(caps[:xpath_functions]).to eq(:complete) # All 27 functions! - end - end -end diff --git a/spec/taurus/cli_spec.rb b/spec/taurus/cli_spec.rb deleted file mode 100644 index 45c6138..0000000 --- a/spec/taurus/cli_spec.rb +++ /dev/null @@ -1,318 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require "taurus/cli" -require "stringio" -require "fileutils" - -RSpec.describe Taurus::CLI do - let(:xml_file) { "spec/fixtures/books.xml" } - let(:xml_content) do - <<~XML - - - - The Ruby Way - Hal Fulton - - - Programming Ruby - Dave Thomas - - - XML - end - - before do - # Create temporary XML file for testing - FileUtils.mkdir_p("spec/fixtures") - File.write(xml_file, xml_content) - end - - after do - # Clean up - FileUtils.rm_f(xml_file) - end - - describe "version command" do - it "displays version information" do - expect { described_class.start(["version"]) }.to output(/Taurus #{Taurus::VERSION}/).to_stdout - end - - it "includes description" do - expect { described_class.start(["version"]) }.to output(/Fast XML parser/).to_stdout - end - end - - describe "xpath command" do - context "with basic XPath query" do - it "executes simple path query" do - output = capture_stdout { described_class.start(["xpath", xml_file, "//book"]) } - expect(output).to include(" 25]"]) } - expect(output).to include("Programming Ruby") - expect(output).not_to include("The Ruby Way") - end - end - - context "with stdin input" do - it "reads from stdin when filename is -" do - allow($stdin).to receive(:read).and_return(xml_content) - output = capture_stdout { described_class.start(["xpath", "-", "//book"]) } - expect(output).to include("Test" } - let(:compact_file) { "spec/fixtures/compact.xml" } - - before do - File.write(compact_file, compact_xml) - end - - after do - FileUtils.rm_f(compact_file) - FileUtils.rm_f("spec/fixtures/formatted.xml") - end - - context "with default options" do - it "formats XML with 2-space indentation" do - output = capture_stdout { described_class.start(["format", compact_file]) } - expect(output).to include(" ") - end - - it "preserves attributes" do - output = capture_stdout { described_class.start(["format", compact_file]) } - expect(output).to include('id="1"') - end - - it "adds proper line breaks" do - output = capture_stdout { described_class.start(["format", compact_file]) } - lines = output.split("\n") - expect(lines.length).to be > 3 - end - end - - context "with custom indentation" do - it "formats with 4-space indentation with --indent 4" do - output = capture_stdout { described_class.start(["format", "--indent", "4", compact_file]) } - expect(output).to include(" ") - end - - it "works with -i alias" do - output = capture_stdout { described_class.start(["format", "-i", "4", compact_file]) } - expect(output).to include(" ' } - let(:doc) { parse(xml) } - - it "finds root element" do - result = doc.xpath('/root') - expect(result.size).to eq(1) - expect(result.first).to be_a(Taurus::Element) - expect(result.first.name).to eq('root') - end - - it "navigates multiple levels from document" do - result = doc.xpath('/root/child/item') - expect(result.size).to eq(1) - expect(result.first.name).to eq('item') - end - - it "returns empty for non-existent path" do - result = doc.xpath('/nonexistent') - expect(result).to be_empty - end - - it "handles absolute path with //" do - xml = '' - doc = parse(xml) - result = doc.xpath('//item') - expect(result.size).to eq(2) - end - end - - describe "descendant search from document" do - let(:xml) do - <<~XML - - - - - - - - - - XML - end - - let(:doc) { parse(xml) } - - it "finds all matching descendants" do - result = doc.xpath('//book') - expect(result.size).to eq(3) - end - - it "finds all elements with wildcard" do - result = doc.xpath('//*') - expect(result.size).to be >= 4 # catalog, books, music, 3 books - end - - it "combines descendant search with name" do - result = doc.xpath('//books/book') - expect(result.size).to eq(2) - end - end - - describe "document as context node" do - let(:xml) { '' } - let(:doc) { parse(xml) } - - it "uses document as starting point" do - expect(doc.xpath('/root')).not_to be_empty - end - - it "returns consistent results from document context" do - doc_result = doc.xpath('//item') - root_result = doc.root.xpath('//item') - expect(doc_result.size).to eq(root_result.size) - end - end - - describe "root element access" do - let(:xml) { '' } - let(:doc) { parse(xml) } - - it "can query root element" do - result = doc.xpath('/library') - expect(result.first).to eq(doc.root) - end - - it "accesses root's children" do - result = doc.xpath('/library/book') - expect(result.size).to eq(2) - end - end - - describe "complex document queries" do - let(:xml) do - <<~XML - - - Alice - Bob - - - Post 1 - Post 2 - Post 3 - - - XML - end - - let(:doc) { parse(xml) } - - it "queries across different branches" do - users = doc.xpath('//user') - posts = doc.xpath('//post') - expect(users.size).to eq(2) - expect(posts.size).to eq(3) - end - - it "handles multi-level absolute paths" do - result = doc.xpath('/database/users/user') - expect(result.size).to eq(2) - end - - it "finds elements at different depths" do - result = doc.xpath('//database/*/user') - expect(result.size).to eq(2) - end - end - - describe "attribute queries from document" do - let(:xml) { '' } - let(:doc) { parse(xml) } - - it "selects attributes from document context" do - result = doc.xpath('//item/@id') - expect(result).to be_an(Array) - expect(result).to eq(['1', '2']) - end - - it "finds all attributes in document" do - xml = '' - doc = parse(xml) - result = doc.xpath('//@*') - expect(result.size).to eq(2) - end - end - - describe "edge cases with document" do - it "handles empty document" do - doc = Taurus::Document.new - result = doc.xpath('/*') - expect(result).to be_empty - end - - it "handles single element document" do - doc = parse('') - result = doc.xpath('/root') - expect(result.size).to eq(1) - end - - it "handles whitespace in queries" do - doc = parse('') - # XPath should handle whitespace in paths - result = doc.xpath('/root/item') - expect(result.size).to eq(1) - end - end - - describe "document-level navigation" do - let(:xml) do - <<~XML - -
- - Novel 1 - -
-
- XML - end - - let(:doc) { parse(xml) } - - it "navigates from document to deep elements" do - result = doc.xpath('/library/section/shelf/book') - expect(result.size).to eq(1) - expect(result.first.text).to eq('Novel 1') - end - - it "uses descendant-or-self from document" do - result = doc.xpath('//book') - expect(result.size).to eq(1) - end - end - - describe "consistency with element queries" do - let(:xml) { '' } - let(:doc) { parse(xml) } - - it "produces same results as equivalent element query" do - doc_result = doc.xpath('//b') - root_result = doc.root.xpath('.//b') - expect(doc_result).to eq(root_result) - end - - it "handles self from document appropriately" do - # Document.xpath with '.' may have implementation-specific behavior - result = doc.xpath('.') - # Result should either be empty or [doc] - expect(result).to be_an(Array) - end - end - - describe "performance from document context" do - it "efficiently queries large documents" do - # Build a moderately large document - items = (1..50).map { |i| "Item #{i}" }.join - xml = "#{items}" - doc = parse(xml) - - start = Time.now - result = doc.xpath('//item') - elapsed = Time.now - start - - expect(result.size).to eq(50) - expect(elapsed).to be < 0.5 # Should be fast - end - end -end \ No newline at end of file diff --git a/spec/taurus/element_spec.rb b/spec/taurus/element_spec.rb deleted file mode 100644 index b4012d2..0000000 --- a/spec/taurus/element_spec.rb +++ /dev/null @@ -1,322 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe Taurus::Element do - describe "parent manipulation" do - it "sets parent correctly" do - parent = Taurus::Element.new("parent") - child = Taurus::Element.new("child") - - child.parent = parent - - expect(child.parent).to eq(parent) - end - - it "prevents circular references" do - elem1 = Taurus::Element.new("elem1") - elem2 = Taurus::Element.new("elem2") - elem3 = Taurus::Element.new("elem3") - - elem1.parent = elem2 - elem2.parent = elem3 - - # This should not create a circular reference - original_parent = elem3.parent - elem3.parent = elem1 - - expect(elem3.parent).to eq(original_parent) # Parent should not change - end - - it "allows setting parent to nil" do - parent = Taurus::Element.new("parent") - child = Taurus::Element.new("child") - - child.parent = parent - expect(child.parent).to eq(parent) - - child.parent = nil - expect(child.parent).to be_nil - end - end - - describe "node manipulation" do - it "adds child elements" do - parent = Taurus::Element.new("parent") - child = Taurus::Element.new("child") - - parent << child - - expect(parent.nodes).to include(child) - expect(child.parent).to eq(parent) - end - - it "removes elements from parent" do - parent = Taurus::Element.new("parent") - child = Taurus::Element.new("child") - - parent << child - expect(parent.nodes).to include(child) - expect(child.parent).to eq(parent) - - child.remove - expect(parent.nodes).not_to include(child) - expect(child.parent).to be_nil - end - - it "adds child with add_child method" do - parent = Taurus::Element.new("parent") - child = Taurus::Element.new("child") - - parent.add_child(child) - - expect(parent.nodes).to include(child) - expect(child.parent).to eq(parent) - end - - it "removes child from old parent when adding to new parent" do - old_parent = Taurus::Element.new("old_parent") - new_parent = Taurus::Element.new("new_parent") - child = Taurus::Element.new("child") - - old_parent.add_child(child) - expect(old_parent.nodes).to include(child) - expect(child.parent).to eq(old_parent) - - new_parent.add_child(child) - expect(old_parent.nodes).not_to include(child) - expect(new_parent.nodes).to include(child) - expect(child.parent).to eq(new_parent) - end - end - - describe "text content" do - it "returns text content" do - elem = Taurus::Element.new("test") - elem << "Hello World" - - expect(elem.text).to eq("Hello World") - end - - it "returns nil when no text content" do - elem = Taurus::Element.new("test") - - expect(elem.text).to be_nil - end - - it "replaces text content" do - elem = Taurus::Element.new("test") - elem << "Old text" - elem << Taurus::Element.new("child") - - elem.replace_text("New text") - - expect(elem.nodes).to eq(["New text"]) - expect(elem.text).to eq("New text") - end - end - - describe "attributes" do - it "sets and gets attributes" do - elem = Taurus::Element.new("test") - - elem["name"] = "value" - expect(elem["name"]).to eq("value") - end - - it "supports symbol keys" do - elem = Taurus::Element.new("test") - - elem[:name] = "value" - expect(elem[:name]).to eq("value") - expect(elem["name"]).to eq("value") - end - end - - describe "equality" do - it "compares equal elements" do - elem1 = Taurus::Element.new("test") - elem2 = Taurus::Element.new("test") - - expect(elem1).to eq(elem2) - end - - it "compares different elements" do - elem1 = Taurus::Element.new("test1") - elem2 = Taurus::Element.new("test2") - - expect(elem1).not_to eq(elem2) - end - end - - describe "element name interning (Session 72 optimization)" do - it "returns interned (frozen) strings for parsed element names" do - xml = "" - doc = Taurus.parse(xml) - - name1 = doc.root.nodes[0].name - name2 = doc.root.nodes[1].name - - # Names should be frozen (interned) - expect(name1).to be_frozen - expect(name2).to be_frozen - - # Same element names should be same object (interned) - expect(name1.object_id).to eq(name2.object_id) - end - - it "interned names work with string comparison" do - xml = "" - doc = Taurus.parse(xml) - - expect(doc.root.name).to eq("root") - expect(doc.root.nodes.first.name).to eq("item") - end - - it "different element names are different objects" do - xml = "" - doc = Taurus.parse(xml) - - name1 = doc.root.nodes[0].name - name2 = doc.root.nodes[1].name - - expect(name1).to eq("item") - expect(name2).to eq("other") - expect(name1.object_id).not_to eq(name2.object_id) - end - - it "root element name is also interned" do - xml = "" - doc = Taurus.parse(xml) - - root_name = doc.root.name - child_name = doc.root.nodes.first.name - - expect(root_name).to be_frozen - expect(child_name).to be_frozen - expect(root_name.object_id).to eq(child_name.object_id) - end - - describe "attribute access optimization (Session 73)" do - let(:elem) do - Taurus::Element.new("test").tap do |e| - e[:name] = "value" - e[:count] = "42" - e[:enabled] = "true" - end - end - - it "symbol access uses fast-path (no conversion)" do - # Fast path: symbol → direct lookup - expect(elem[:name]).to eq("value") - expect(elem[:count]).to eq("42") - expect(elem[:enabled]).to eq("true") - end - - it "string access still works (backwards compatible)" do - # Slow path: string → convert to symbol - expect(elem["name"]).to eq("value") - expect(elem["count"]).to eq("42") - expect(elem["enabled"]).to eq("true") - end - - it "mixed access works correctly" do - elem[:fast] = "symbol" - elem["slow"] = "string" - - expect(elem[:fast]).to eq("symbol") - expect(elem["slow"]).to eq("string") - expect(elem[:slow]).to eq("string") # Symbol access to string-set attr - expect(elem["fast"]).to eq("symbol") # String access to symbol-set attr - end - - it "nil for missing attributes (both paths)" do - expect(elem[:missing]).to be_nil - expect(elem["missing"]).to be_nil - end - - it "handles parsed document attributes with symbol fast-path" do - xml = '' - doc = Taurus.parse(xml) - item = doc.root.nodes.first - - # Should use fast-path for symbol access - expect(item[:id]).to eq("123") - expect(item[:name]).to eq("test") - - # String access should still work - expect(item["id"]).to eq("123") - expect(item["name"]).to eq("test") - end - - it "multiple attribute reads use fast-path consistently" do - # This tests that multiple reads benefit from fast-path - 10.times do - expect(elem[:name]).to eq("value") - expect(elem[:count]).to eq("42") - end - end - end - - describe "children access optimization (Session 74)" do - it "nodes array is initialized in Ruby constructor" do - elem = Taurus::Element.new("test") - - # @nodes should be initialized immediately (not lazy) - expect(elem.nodes).to be_an(Array) - expect(elem.nodes).to be_empty - end - - it "nodes array is initialized in C parser" do - xml = "" - doc = Taurus.parse(xml) - - # @nodes should be initialized by C create_element - expect(doc.root.nodes).to be_an(Array) - expect(doc.root.nodes.size).to eq(1) - end - - it "nodes array works with direct access (no lazy init)" do - elem = Taurus::Element.new("test") - child1 = Taurus::Element.new("child1") - child2 = Taurus::Element.new("child2") - - elem << child1 - elem << child2 - - # Multiple reads should work without lazy check - expect(elem.nodes.size).to eq(2) - expect(elem.nodes.size).to eq(2) - expect(elem.nodes).to include(child1, child2) - end - - it "empty element has empty nodes array (not nil)" do - elem = Taurus::Element.new("test") - - # Should return empty array, not nil - expect(elem.nodes).not_to be_nil - expect(elem.nodes).to eq([]) - end - - it "parsed empty element has empty nodes array" do - xml = "" - doc = Taurus.parse(xml) - empty = doc.root.nodes.first - - # Even empty elements should have initialized @nodes - expect(empty.nodes).to be_an(Array) - expect(empty.nodes).to be_empty - end - - it "nodes array identity is consistent (same object)" do - elem = Taurus::Element.new("test") - - # Should return same array object every time (direct access) - array1 = elem.nodes - array2 = elem.nodes - - expect(array1.object_id).to eq(array2.object_id) - end - end - end -end \ No newline at end of file diff --git a/spec/taurus/element_xpath_namespace_spec.rb b/spec/taurus/element_xpath_namespace_spec.rb deleted file mode 100644 index 669cda6..0000000 --- a/spec/taurus/element_xpath_namespace_spec.rb +++ /dev/null @@ -1,297 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Taurus::Element, '#xpath with namespace prefixes (v0.8.0)' do - describe 'Basic namespace prefix support' do - let(:xml) do - <<~XML - - XPath Guide - 123-456 - John Doe - john@example.com - - XML - end - - let(:doc) { parse(xml) } - - it 'finds elements by namespace prefix' do - result = doc.xpath('//book:title') - expect(result.size).to eq(1) - expect(result[0].text).to eq('XPath Guide') - end - - it 'distinguishes between same local names in different namespaces' do - # Both have 'title' as local name but different namespaces - xml_multi = <<~XML - - Book Title - Article Title - - XML - - doc_multi = parse(xml_multi) - - book_titles = doc_multi.xpath('//book:title') - article_titles = doc_multi.xpath('//article:title') - - expect(book_titles.size).to eq(1) - expect(article_titles.size).to eq(1) - expect(book_titles[0].text).to eq('Book Title') - expect(article_titles[0].text).to eq('Article Title') - end - - it 'supports multiple elements with same prefix' do - result = doc.xpath('//book:*') - expect(result.size).to eq(2) - expect(result.map(&:text).sort).to eq(['123-456', 'XPath Guide']) - end - - it 'supports wildcard with namespace prefix' do - author_elements = doc.xpath('//author:*') - expect(author_elements.size).to eq(2) - expect(author_elements[0].text).to eq('John Doe') - expect(author_elements[1].text).to eq('john@example.com') - end - end - - describe 'Namespace prefixes in predicates' do - let(:xml) do - <<~XML - -
- First Book - Book description -
-
- No book here -
-
- XML - end - - let(:doc) { parse(xml) } - - it 'works in predicates' do - result = doc.xpath('//section[book:title]') - expect(result.size).to eq(1) - expect(result[0].xpath('.//book:title')[0].text).to eq('First Book') - end - - it 'combines prefix with attribute predicates' do - xml_with_attrs = <<~XML - - First - Second - Third - - XML - - doc_attrs = parse(xml_with_attrs) - result = doc_attrs.xpath('//book:item[@id]') - expect(result.size).to eq(2) - end - end - - describe 'Nested namespaces' do - let(:xml) do - <<~XML - - - Item 1 - Item 2 - - - XML - end - - let(:doc) { parse(xml) } - - it 'handles nested namespace declarations' do - inner_items = doc.xpath('//inner:item') - outer_items = doc.xpath('//outer:item') - - expect(inner_items.size).to eq(1) - expect(outer_items.size).to eq(1) - expect(inner_items[0].text).to eq('Item 1') - expect(outer_items[0].text).to eq('Item 2') - end - - it 'finds all items regardless of namespace' do - # Without prefix, should match local name only - all_items = doc.xpath('//item') - expect(all_items.size).to eq(2) - end - end - - describe 'Default namespace' do - let(:xml) do - <<~XML - - Default NS Item - Special NS Item - - XML - end - - let(:doc) { parse(xml) } - - it 'matches elements without prefix using local name' do - # Elements in default namespace should match by local name - items = doc.xpath('//item') - expect(items.size).to eq(2) - end - - it 'matches prefixed elements explicitly' do - special_items = doc.xpath('//special:item') - expect(special_items.size).to eq(1) - expect(special_items[0].text).to eq('Special NS Item') - end - end - - describe 'Complex queries with namespaces' do - let(:xml) do - <<~XML - - - Learning XPath - Jane Smith - - - Tech Weekly - Bob Johnson - - - Advanced XPath - Jane Smith - - - XML - end - - let(:doc) { parse(xml) } - - it 'combines namespace prefix with attribute filters' do - result = doc.xpath('//book:publication[@year="2020"]') - expect(result.size).to eq(1) - expect(result[0].xpath('.//book:title')[0].text).to eq('Learning XPath') - end - - it 'finds all publications regardless of namespace' do - all_pubs = doc.xpath('//publication') - expect(all_pubs.size).to eq(3) - end - - it 'chains namespace-aware queries' do - # Find book publications, then their titles - book_titles = doc.xpath('//book:publication/book:title') - expect(book_titles.size).to eq(2) - expect(book_titles.map(&:text).sort).to eq(['Advanced XPath', 'Learning XPath']) - end - - it 'uses position predicates with namespace prefixes' do - first_book = doc.xpath('//book:publication[1]') - expect(first_book.size).to eq(1) - expect(first_book[0][:year]).to eq('2020') - end - - it 'combines wildcards with namespace prefixes' do - # All children of book:publication elements - book_children = doc.xpath('//book:publication/book:*') - expect(book_children.size).to eq(4) # 2 publications × (title + author) - end - end - - describe 'Descendant axis with namespaces' do - let(:xml) do - <<~XML - - - - Found It! - - - - XML - end - - let(:doc) { parse(xml) } - - it 'finds descendants with namespace prefix' do - result = doc.xpath('//ns:target') - expect(result.size).to eq(1) - expect(result[0].text).to eq('Found It!') - end - - it 'finds all descendants regardless of namespace' do - all_targets = doc.xpath('//target') - expect(all_targets.size).to eq(1) - end - end - - describe 'Multiple namespace prefixes on same element' do - let(:xml) do - <<~XML - - Item A - Item B - Item C - No namespace - - XML - end - - let(:doc) { parse(xml) } - - it 'distinguishes elements with different namespace prefixes' do - a_items = doc.xpath('//a:item') - b_items = doc.xpath('//b:item') - c_items = doc.xpath('//c:item') - - expect(a_items.size).to eq(1) - expect(b_items.size).to eq(1) - expect(c_items.size).to eq(1) - - expect(a_items[0].text).to eq('Item A') - expect(b_items[0].text).to eq('Item B') - expect(c_items[0].text).to eq('Item C') - end - - it 'finds items without prefix using local name' do - no_prefix = doc.xpath('//item') - expect(no_prefix.size).to eq(4) # All items including namespaced ones - end - end - - describe 'Backward compatibility' do - let(:xml) do - <<~XML - - Plain Item 1 - Plain Item 2 - - XML - end - - let(:doc) { parse(xml) } - - it 'works without namespaces (backward compatible)' do - items = doc.xpath('//item') - expect(items.size).to eq(2) - end - - it 'matches local names when no namespace is used' do - result = doc.xpath('//item[1]') - expect(result.size).to eq(1) - expect(result[0].text).to eq('Plain Item 1') - end - end -end \ No newline at end of file diff --git a/spec/taurus/element_xpath_spec.rb b/spec/taurus/element_xpath_spec.rb deleted file mode 100644 index eb6f874..0000000 --- a/spec/taurus/element_xpath_spec.rb +++ /dev/null @@ -1,1917 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Taurus::Element, "#xpath" do - describe "basic path queries" do - let(:xml) do - <<~XML - -
- First - Second -
- - Third - -
- XML - end - - let(:doc) { parse(xml) } - let(:root) { doc.root } - let(:section) { root.nodes.first } - - it "returns array of matching elements" do - result = root.xpath('section') - expect(result).to be_an(Array) - expect(result.size).to eq(1) - expect(result.first).to be_a(Taurus::Element) - expect(result.first.name).to eq('section') - end - - it "returns empty array for no matches" do - result = root.xpath('nonexistent') - expect(result).to eq([]) - end - - it "handles multi-level paths" do - result = root.xpath('section/item') - expect(result.size).to eq(2) - end - - it "returns correct element from relative path" do - result = section.xpath('item') - expect(result.size).to eq(2) - expect(result.map(&:name)).to eq(['item', 'item']) - end - end - - describe "child axis" do - let(:xml) { '' } - let(:doc) { parse(xml) } - let(:root) { doc.root } - - it "returns all direct children" do - result = root.xpath('child::*') - expect(result.size).to eq(3) - expect(result.map(&:name)).to eq(['a', 'b', 'c']) - end - - it "returns children matching name" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('child::item') - expect(result.size).to eq(2) - expect(result.map(&:name)).to eq(['item', 'item']) - end - - it "returns empty for childless element" do - result = root.xpath('a/child::*') - expect(result).to be_empty - end - - it "works with explicit child axis" do - result = root.xpath('child::b') - expect(result.size).to eq(1) - expect(result.first.name).to eq('b') - end - end - - describe "descendant search (//)" do - let(:xml) do - <<~XML - -
- Book1 - Book2 -
- - Book3 - -
- XML - end - - let(:doc) { parse(xml) } - let(:root) { doc.root } - - it "finds all descendants matching name" do - result = root.xpath('.//book') - expect(result.size).to eq(3) - expect(result.map(&:name)).to all(eq('book')) - end - - it "finds descendants from element context" do - section = root.nodes.first - result = section.xpath('.//book') - expect(result.size).to eq(2) - end - - it "works with absolute descendant path" do - result = doc.xpath('//book') - expect(result.size).to eq(3) - end - - it "finds all descendants with wildcard" do - result = root.xpath('.//*') - expect(result.size).to be >= 5 - end - end - - describe "descendant-or-self axis" do - let(:xml) { '
' } - let(:doc) { parse(xml) } - let(:root) { doc.root } - - it "includes context node in results" do - result = root.xpath('descendant-or-self::*') - expect(result.first).to eq(root) - end - - it "includes all descendants" do - result = root.xpath('descendant-or-self::*') - expect(result.size).to eq(3) # root, a, b - expect(result.map(&:name)).to eq(['root', 'a', 'b']) - end - - it "works from mid-tree element" do - a = navigate_to(root, 'a') - result = a.xpath('descendant-or-self::*') - expect(result.size).to eq(2) # a, b - expect(result.map(&:name)).to eq(['a', 'b']) - end - end - - describe "descendant axis (descendant::*)" do - it "excludes context node, includes all descendants" do - doc = parse('') - result = doc.root.xpath('descendant::*') - expect(result.size).to eq(3) # a, b, container (NOT root) - expect(result.map(&:name)).to eq(['a', 'b', 'container']) - end - - it "works with deep nesting" do - doc = parse('') - result = doc.root.xpath('descendant::*') - expect(result.size).to eq(3) # level1, level2, level3 - expect(result.map(&:name)).to eq(['level1', 'level2', 'level3']) - end - - it "works from non-root context" do - doc = parse('') - a = doc.root.xpath('a').first - result = a.xpath('descendant::*') - expect(result.size).to eq(2) # b, c (NOT a) - expect(result.map(&:name)).to eq(['b', 'c']) - end - - it "works with multiple branches" do - doc = parse('') - result = doc.root.xpath('descendant::*') - expect(result.size).to eq(4) # a, aa, b, bb - end - - it "returns empty for leaf element" do - doc = parse('') - leaf = doc.root.xpath('leaf').first - result = leaf.xpath('descendant::*') - expect(result).to be_empty - end - - it "works with name test" do - doc = parse('') - result = doc.root.xpath('descendant::a') - expect(result.size).to eq(2) - expect(result.map(&:name)).to eq(['a', 'a']) - end - end - - describe "ancestor-or-self axis (ancestor-or-self::*)" do - it "includes context node in results" do - doc = parse('
') - c = navigate_to(doc.root, 'a/b/c') - result = c.xpath('ancestor-or-self::*') - expect(result.first).to eq(c) - expect(result.map(&:name)).to eq(['c', 'b', 'a', 'root']) - end - - it "returns just self for root element" do - doc = parse('') - result = doc.root.xpath('ancestor-or-self::*') - expect(result.size).to eq(1) - expect(result.first).to eq(doc.root) - end - - it "walks up the entire ancestor chain" do - doc = parse('') - c = navigate_to(doc.root, 'a/b/c') - result = c.xpath('ancestor-or-self::*') - expect(result.size).to eq(4) # c, b, a, root - end - - it "works with name test" do - doc = parse('
') - item = navigate_to(doc.root, 'section/section/item') - result = item.xpath('ancestor-or-self::section') - expect(result.size).to eq(2) - expect(result.map(&:name)).to all(eq('section')) - end - - it "maintains correct order (self first, then ancestors)" do - doc = parse('') - b = navigate_to(doc.root, 'a/b') - result = b.xpath('ancestor-or-self::*') - expect(result.map(&:name)).to eq(['b', 'a', 'root']) - end - end - - describe "ancestor axis (ancestor::*)" do - it "excludes context node, includes all ancestors" do - doc = parse('') - c = navigate_to(doc.root, 'a/b/c') - result = c.xpath('ancestor::*') - expect(result.size).to eq(3) # b, a, root (NOT c) - expect(result.map(&:name)).to eq(['b', 'a', 'root']) - end - - it "returns empty for root element" do - doc = parse('') - result = doc.root.xpath('ancestor::*') - expect(result).to be_empty - end - - it "walks up entire ancestor chain" do - doc = parse('') - l4 = navigate_to(doc.root, 'l1/l2/l3/l4') - result = l4.xpath('ancestor::*') - expect(result.size).to eq(4) # l3, l2, l1, root - expect(result.map(&:name)).to eq(['l3', 'l2', 'l1', 'root']) - end - - it "works with name test" do - doc = parse('
') - span = navigate_to(doc.root, 'div/div/span') - result = span.xpath('ancestor::div') - expect(result.size).to eq(2) - expect(result.map(&:name)).to all(eq('div')) - end - - it "works from intermediate element" do - doc = parse('') - b = navigate_to(doc.root, 'a/b') - result = b.xpath('ancestor::*') - expect(result.size).to eq(2) # a, root (NOT b) - expect(result.map(&:name)).to eq(['a', 'root']) - end - end - - describe "following-sibling axis (following-sibling::*)" do - it "returns siblings after context node" do - doc = parse('') - b = navigate_to(doc.root, 'b') - result = b.xpath('following-sibling::*') - expect(result.size).to eq(2) # c, d (not a or b) - expect(result.map(&:name)).to eq(['c', 'd']) - end - - it "returns empty for last child" do - doc = parse('
') - c = navigate_to(doc.root, 'c') - result = c.xpath('following-sibling::*') - expect(result).to be_empty - end - - it "maintains document order" do - doc = parse('') - first = navigate_to(doc.root, 'first') - result = first.xpath('following-sibling::*') - expect(result.map(&:name)).to eq(['second', 'third', 'fourth']) - end - - it "works with name test" do - doc = parse('
') - first_a = doc.root.xpath('child::*')[0] - result = first_a.xpath('following-sibling::a') - expect(result.size).to eq(2) - expect(result.map(&:name)).to all(eq('a')) - end - - it "only returns elements, not text nodes" do - doc = parse('textmore text
') - a = navigate_to(doc.root, 'a') - result = a.xpath('following-sibling::*') - expect(result.size).to eq(2) # b, c (not text) - expect(result.map(&:name)).to eq(['b', 'c']) - end - end - - describe "preceding-sibling axis (preceding-sibling::*)" do - it "returns siblings before context node" do - doc = parse('') - c = navigate_to(doc.root, 'c') - result = c.xpath('preceding-sibling::*') - expect(result.size).to eq(2) # a, b (not c or d) - expect(result.map(&:name)).to eq(['a', 'b']) - end - - it "returns empty for first child" do - doc = parse('') - a = navigate_to(doc.root, 'a') - result = a.xpath('preceding-sibling::*') - expect(result).to be_empty - end - - it "maintains document order" do - doc = parse('') - fourth = navigate_to(doc.root, 'fourth') - result = fourth.xpath('preceding-sibling::*') - expect(result.map(&:name)).to eq(['first', 'second', 'third']) - end - - it "works with name test" do - doc = parse('') - last_a = doc.root.xpath('child::*')[4] - result = last_a.xpath('preceding-sibling::a') - expect(result.size).to eq(2) - expect(result.map(&:name)).to all(eq('a')) - end - - it "only returns elements, not text nodes" do - doc = parse('textmore text') - c = navigate_to(doc.root, 'c') - result = c.xpath('preceding-sibling::*') - expect(result.size).to eq(2) # a, b (not text) - expect(result.map(&:name)).to eq(['a', 'b']) - end - end - - describe "following axis (following::*)" do - it "includes following siblings" do - doc = parse('') - a = navigate_to(doc.root, 'a') - result = a.xpath('following::*') - expect(result.size).to eq(2) # b, c - expect(result.map(&:name)).to eq(['b', 'c']) - end - - it "includes descendants of following siblings" do - doc = parse('') - a = navigate_to(doc.root, 'a') - result = a.xpath('following::*') - expect(result.size).to eq(4) # b, bb, c, cc - expect(result.map(&:name)).to eq(['b', 'bb', 'c', 'cc']) - end - - it "includes parent's following nodes" do - doc = parse('') - child1 = navigate_to(doc.root, 'parent1/child1') - result = child1.xpath('following::*') - expect(result.size).to eq(2) # parent2, child2 - expect(result.map(&:name)).to eq(['parent2', 'child2']) - end - - it "does not cause infinite recursion" do - doc = parse('') - e = navigate_to(doc.root, 'a/b/c/d/e') - result = e.xpath('following::*') - expect(result).to be_an(Array) - expect(result).to be_empty # e is last, no following - end - - it "maintains correct document order" do - doc = parse('') - a = navigate_to(doc.root, 'a') - result = a.xpath('following::*') - # following axis excludes descendants of context node - # aa is a descendant of a, so not included - expect(result.map(&:name)).to eq(['b', 'c', 'cc']) - end - - it "returns empty for last node in document" do - doc = parse('') - b = navigate_to(doc.root, 'b') - result = b.xpath('following::*') - expect(result).to be_empty - end - end - - describe "preceding axis (preceding::*)" do - it "includes preceding siblings" do - doc = parse('') - c = navigate_to(doc.root, 'c') - result = c.xpath('preceding::*') - expect(result.size).to eq(2) # a, b - expect(result.map(&:name)).to eq(['a', 'b']) - end - - it "includes descendants of preceding siblings" do - doc = parse('') - c = navigate_to(doc.root, 'c') - result = c.xpath('preceding::*') - expect(result.size).to eq(4) # a, aa, b, bb - expect(result.map(&:name)).to eq(['a', 'aa', 'b', 'bb']) - end - - it "includes parent's preceding nodes" do - doc = parse('') - child2 = navigate_to(doc.root, 'parent2/child2') - result = child2.xpath('preceding::*') - expect(result.size).to eq(2) # parent1, child1 - expect(result.map(&:name)).to eq(['parent1', 'child1']) - end - - it "does not cause infinite recursion" do - doc = parse('') - a = navigate_to(doc.root, 'a') - result = a.xpath('preceding::*') - expect(result).to be_an(Array) - expect(result).to be_empty # a is first, no preceding - end - - it "returns empty for first node in document" do - doc = parse('') - a = navigate_to(doc.root, 'a') - result = a.xpath('preceding::*') - expect(result).to be_empty - end - end - - describe "parent navigation (..)" do - let(:xml) { '' } - let(:doc) { parse(xml) } - let(:root) { doc.root } - - it "navigates to parent element" do - child = navigate_to(root, 'parent/child') - result = child.xpath('..') - expect(result.size).to eq(1) - expect(result.first.name).to eq('parent') - end - - it "returns empty for root element" do - result = root.xpath('..') - expect(result).to be_empty - end - - it "works with parent axis" do - child = navigate_to(root, 'parent/child') - result = child.xpath('parent::*') - expect(result.size).to eq(1) - expect(result.first.name).to eq('parent') - end - - it "allows chaining after parent navigation" do - xml = '' - doc = parse(xml) - c = navigate_to(doc.root, 'a/b/c') - result = c.xpath('../..') - expect(result.size).to eq(1) - expect(result.first.name).to eq('a') - end - end - - describe "self axis (.)" do - let(:xml) { '' } - let(:doc) { parse(xml) } - let(:root) { doc.root } - - it "returns context node" do - result = root.xpath('.') - expect(result).to eq([root]) - end - - it "works with self axis" do - result = root.xpath('self::*') - expect(result.size).to eq(1) - expect(result.first).to eq(root) - end - - it "returns self from any element" do - item = root.nodes.first - result = item.xpath('.') - expect(result).to eq([item]) - end - end - - describe "wildcard matching (*)" do - let(:xml) { '' } - let(:doc) { parse(xml) } - let(:root) { doc.root } - - it "matches all children" do - result = root.xpath('*') - expect(result.size).to eq(3) - end - - it "matches all descendants" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('.//*') - expect(result.size).to eq(4) # a, aa, b, bb - end - - it "works in multi-step paths" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath(' */item') - expect(result.size).to eq(1) - end - end - - describe "attribute selection" do - let(:xml) do - <<~XML - - - - - XML - end - - let(:doc) { parse(xml) } - let(:root) { doc.root } - - it "selects specific attribute" do - result = root.xpath('.//book/@id') - expect(result).to be_an(Array) - expect(result).to eq(['1', '2']) - end - - it "selects multiple attributes" do - result = root.xpath('.//book/@title') - expect(result).to eq(['XPath Guide', 'Ruby Guide']) - end - - it "selects all attributes with @*" do - result = root.xpath('.//book/@*') - expect(result.size).to eq(4) # 2 books x 2 attributes each - # Attributes are returned in order: id, title for each book - expect(result).to eq(['1', 'XPath Guide', '2', 'Ruby Guide']) - end - - it "works with attribute axis" do - result = root.xpath('.//book/attribute::id') - expect(result).to eq(['1', '2']) - end - end - - describe "absolute paths from document" do - let(:xml) { '' } - let(:doc) { parse(xml) } - - it "starts from document root with /" do - item = doc.root.nodes.first.nodes.first - result = item.xpath('/root') - # Absolute paths should work because Element#xpath finds the document - expect(result.size).to eq(1) - expect(result.first).to eq(doc.root) - end - - it "navigates multiple levels" do - result = doc.xpath('/root/child/item') - expect(result.size).to eq(1) - end - - it "works with // from any context" do - child = doc.root.nodes.first - result = child.xpath('//item') - expect(result.size).to eq(1) - end - end - - describe "complex queries" do - let(:xml) do - <<~XML - - - Book A - Book B - - - Album A - - - XML - end - - let(:doc) { parse(xml) } - let(:root) { doc.root } - - it "handles multi-step descendant search" do - result = root.xpath('.//category/product') - expect(result.size).to eq(3) - end - - it "combines wildcard with descendant" do - result = root.xpath('.//product') - expect(result.size).to eq(3) - end - - it "works with multiple path steps" do - result = root.xpath('category/product') - expect(result.size).to eq(3) - end - end - - describe "context resolution" do - let(:xml) { '' } - let(:doc) { parse(xml) } - - it "finds document context automatically from deep element" do - c = navigate_to(doc.root, 'a/b/c') - expect { c.xpath('.') }.not_to raise_error - end - - it "allows relative queries from any element" do - b = navigate_to(doc.root, 'a/b') - result = b.xpath('c') - expect(result.size).to eq(1) - end - - it "maintains correct context through navigation" do - a = navigate_to(doc.root, 'a') - result = a.xpath('b/c') - expect(result.size).to eq(1) - expect(result.first.name).to eq('c') - end - end - - describe "edge cases" do - it "handles single element document" do - doc = parse('') - result = doc.root.xpath('.') - expect(result.size).to eq(1) - end - - it "handles empty xpath gracefully" do - doc = parse('') - # Empty XPath should raise an error - expect { doc.root.xpath('') }.to raise_error(RuntimeError, /parsing error/) - end - - it "handles elements with text content" do - doc = parse('text') - result = doc.root.xpath('item') - expect(result.size).to eq(1) - expect(result.first.text).to eq('text') - end - - it "handles mixed content elements" do - doc = parse('text1text2text3') - result = doc.root.xpath('*') - expect(result.size).to eq(2) - expect(result.map(&:name)).to eq(['a', 'b']) - end - - it "handles nested same-named elements" do - doc = parse('') - result = doc.root.xpath('.//item') - expect(result.size).to eq(3) - end - end - - describe "document order" do - let(:xml) do - <<~XML - - - - - - XML - end - - let(:doc) { parse(xml) } - let(:root) { doc.root } - - it "maintains document order in results" do - result = root.xpath('*') - expect(result.map(&:name)).to eq(['z', 'a', 'm']) - end - - it "preserves order in descendant search" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('.//*') - expect(result.map(&:name)).to eq(['b', 'a', 'c']) - end - end - - describe "namespaced elements" do - let(:xml) do - <<~XML - - Content - - XML - end - - let(:doc) { parse(xml) } - - it "finds elements regardless of namespace" do - # Note: Current implementation may not handle namespaces in XPath - # This test documents expected behavior - result = doc.root.xpath('item') - expect(result.size).to be >= 0 # May or may not find namespaced elements - end - end - - describe "performance characteristics" do - it "handles moderately large documents" do - # Create a document with 100 elements - items = (1..100).map { |i| "" }.join - xml = "#{items}" - doc = parse(xml) - - start_time = Time.now - result = doc.root.xpath('.//item') - elapsed = Time.now - start_time - - expect(result.size).to eq(100) - expect(elapsed).to be < 1.0 # Should complete in under 1 second - end - - it "handles deep nesting efficiently" do - # Create deeply nested document - xml = String.new('') - 20.times { |i| xml << "" } - xml << '' - 20.times { |i| xml << "" } - xml << '' - - doc = parse(xml) - result = doc.root.xpath('.//item') - expect(result.size).to eq(1) - end - end - - describe "position predicates" do - let(:xml) { '' } - let(:doc) { parse(xml) } - let(:root) { doc.root } - - it "selects first element with [1]" do - result = root.xpath('*[1]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('a') - end - - it "selects second element with [2]" do - result = root.xpath('*[2]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('b') - end - - it "selects third element with [3]" do - result = root.xpath('*[3]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('c') - end - - it "selects last element with [last()]" do - result = root.xpath('*[last()]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('d') - end - - it "works with named elements" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('item[1]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('item') - end - - it "works with descendant axis" do - xml = '
' - doc = parse(xml) - # descendant::* returns [section, a, b, c] - # Position [2] selects the 2nd one: 'a' - result = doc.root.xpath('descendant::*[2]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('a') - end - - it "works in complex paths" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('section/item[2]') - expect(result.size).to eq(1) - end - - it "applies to result nodeset correctly" do - xml = '
' - doc = parse(xml) - # After selecting all children, [2] should give second one - result = doc.root.xpath('*[2]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('b') - end - - it "handles position with absolute path" do - xml = '' - doc = parse(xml) - result = doc.xpath('//item[1]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('item') - end - - it "returns empty when position is out of range" do - result = root.xpath('*[10]') - expect(result).to be_empty - end - end - - describe "boolean predicates" do - it "filters by attribute existence" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('*[@id]') - expect(result.size).to eq(2) - expect(result.map(&:name)).to eq(['a', 'c']) - end - - it "works with multiple attributes" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('*[@class]') - expect(result.size).to eq(2) - expect(result.map(&:name)).to eq(['a', 'b']) - end - - it "filters by child element existence" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('section[book]') - expect(result.size).to eq(2) - end - - it "works with nested child tests" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('container[span]') - expect(result.size).to eq(2) - end - - it "returns empty when no matches" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('*[@id]') - expect(result).to be_empty - end - - it "works with descendant axis" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('descendant::*[@id]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('item') - end - - it "works with specific element" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('item[@id]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('item') - end - - it "works in complex paths" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('section/item[@id]') - expect(result.size).to eq(1) - end - end - - describe "XPath string functions" do - describe "string()" do - it "converts context node to string with no arguments" do - xml = 'Hello' - doc = parse(xml) - item = doc.root.xpath('item').first - # Call from Ruby: string() should return text content - result = doc.xpath('string(//item)') - expect(result).to eq('Hello') - end - - it "converts number to string" do - doc = parse('') - result = doc.xpath('string(42)') - expect(result).to eq('42') - end - - it "converts boolean true to 'true'" do - doc = parse('') - # Use a comparison that returns true - result = doc.xpath('string(1 = 1)') - expect(result).to eq('true') - end - - it "converts boolean false to 'false'" do - doc = parse('') - result = doc.xpath('string(1 = 2)') - expect(result).to eq('false') - end - - it "handles NaN" do - doc = parse('') - # Division by string that can't convert to number produces NaN - result = doc.xpath('string(0 div 0)') - expect(result).to eq('NaN') - end - - it "handles Infinity" do - doc = parse('') - result = doc.xpath('string(1 div 0)') - expect(result).to eq('Infinity') - end - - it "handles negative Infinity" do - doc = parse('') - result = doc.xpath('string(-1 div 0)') - expect(result).to eq('-Infinity') - end - end - - describe "concat()" do - it "concatenates two strings" do - doc = parse('') - result = doc.xpath("concat('Hello', ' World')") - expect(result).to eq('Hello World') - end - - it "concatenates three strings" do - doc = parse('') - result = doc.xpath("concat('A', 'B', 'C')") - expect(result).to eq('ABC') - end - - it "concatenates many strings" do - doc = parse('') - result = doc.xpath("concat('1', '2', '3', '4', '5')") - expect(result).to eq('12345') - end - - it "converts numbers to strings" do - doc = parse('') - result = doc.xpath("concat('Value: ', 42)") - expect(result).to eq('Value: 42') - end - - it "handles empty strings" do - doc = parse('') - result = doc.xpath("concat('', 'test', '')") - expect(result).to eq('test') - end - end - - describe "starts-with()" do - it "returns true for matching prefix" do - doc = parse('') - result = doc.xpath("starts-with('Hello World', 'Hello')") - expect(result).to eq(true) - end - - it "returns false for non-matching prefix" do - doc = parse('') - result = doc.xpath("starts-with('Hello World', 'World')") - expect(result).to eq(false) - end - - it "returns true for empty prefix" do - doc = parse('') - result = doc.xpath("starts-with('test', '')") - expect(result).to eq(true) - end - - it "works in predicates with attributes" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('item[starts-with(@id, "book")]') - expect(result.size).to eq(2) - end - - it "is case sensitive" do - doc = parse('') - result = doc.xpath("starts-with('Hello', 'hello')") - expect(result).to eq(false) - end - end - - describe "contains()" do - it "returns true when substring is present" do - doc = parse('') - result = doc.xpath("contains('Hello World', 'lo Wo')") - expect(result).to eq(true) - end - - it "returns false when substring is absent" do - doc = parse('') - result = doc.xpath("contains('Hello World', 'xyz')") - expect(result).to eq(false) - end - - it "returns true for empty substring" do - doc = parse('') - result = doc.xpath("contains('test', '')") - expect(result).to eq(true) - end - - it "works in predicates with attributes" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('item[contains(@name, "pie")]') - expect(result.size).to eq(2) - end - - it "is case sensitive" do - doc = parse('') - result = doc.xpath("contains('Hello', 'HEL')") - expect(result).to eq(false) - end - end - - describe "substring()" do - # Basic tests - it "extracts substring with position and length" do - doc = parse('') - result = doc.xpath("substring('12345', 2, 3)") - expect(result).to eq('234') - end - - it "extracts from position to end without length" do - doc = parse('') - result = doc.xpath("substring('12345', 2)") - expect(result).to eq('2345') - end - - it "uses 1-based indexing" do - doc = parse('') - result = doc.xpath("substring('12345', 1, 1)") - expect(result).to eq('1') - end - - # Edge cases - it "handles position 0 with length" do - doc = parse('') - # Position 0 with length 3: positions 0,1,2 → chars at 1,2 - result = doc.xpath("substring('12345', 0, 3)") - expect(result).to eq('12') - end - - it "handles negative position" do - doc = parse('') - # Position -1 with length 4: positions [-1, 3), intersect with [1, ∞) → [1, 3) - # Characters at positions 1,2 → "12" - result = doc.xpath("substring('12345', -1, 4)") - expect(result).to eq('12') - end - - it "returns empty string for position beyond length" do - doc = parse('') - result = doc.xpath("substring('12345', 10, 2)") - expect(result).to eq('') - end - - it "returns empty string for zero length" do - doc = parse('') - result = doc.xpath("substring('12345', 2, 0)") - expect(result).to eq('') - end - - it "returns empty string for negative length" do - doc = parse('') - result = doc.xpath("substring('12345', 2, -1)") - expect(result).to eq('') - end - - # XPath spec edge cases with rounding - it "rounds fractional positions per XPath spec" do - doc = parse('') - # Position 1.5 rounds to 2, length 2.6 rounds to 3 - result = doc.xpath("substring('12345', 1.5, 2.6)") - expect(result).to eq('234') - end - - # NaN handling - it "returns empty string for NaN position" do - doc = parse('') - result = doc.xpath("substring('12345', 0 div 0)") - expect(result).to eq('') - end - - it "returns empty string for NaN length" do - doc = parse('') - result = doc.xpath("substring('12345', 2, 0 div 0)") - expect(result).to eq('') - end - - # UTF-8 character handling - it "counts UTF-8 characters not bytes" do - doc = parse('') - # '你好世界' is 4 characters but 12 bytes in UTF-8 - result = doc.xpath("substring('你好世界', 2, 2)") - expect(result).to eq('好世') - end - - it "handles mixed ASCII and UTF-8" do - doc = parse('') - result = doc.xpath("substring('Hello世界', 6, 2)") - expect(result).to eq('世界') - end - end - - describe "string-length()" do - it "returns length of string" do - doc = parse('') - result = doc.xpath("string-length('Hello')") - expect(result).to eq(5) - end - - it "returns 0 for empty string" do - doc = parse('') - result = doc.xpath("string-length('')") - expect(result).to eq(0) - end - - it "counts UTF-8 characters not bytes" do - doc = parse('') - # '你好' is 2 characters but 6 bytes - result = doc.xpath("string-length('你好')") - expect(result).to eq(2) - end - - it "uses context node when no argument" do - xml = 'Test' - doc = parse(xml) - result = doc.xpath('string-length(//item)') - expect(result).to eq(4) - end - - it "handles mixed ASCII and UTF-8" do - doc = parse('') - result = doc.xpath("string-length('Hello世界')") - expect(result).to eq(7) - end - end - - describe "normalize-space()" do - it "strips leading whitespace" do - doc = parse('') - result = doc.xpath("normalize-space(' test')") - expect(result).to eq('test') - end - - it "strips trailing whitespace" do - doc = parse('') - result = doc.xpath("normalize-space('test ')") - expect(result).to eq('test') - end - - it "collapses internal whitespace to single space" do - doc = parse('') - result = doc.xpath("normalize-space('hello world')") - expect(result).to eq('hello world') - end - - it "handles multiple types of whitespace" do - doc = parse('') - result = doc.xpath("normalize-space(' hello\t\n\rworld ')") - expect(result).to eq('hello world') - end - - it "handles string with no extra whitespace" do - doc = parse('') - result = doc.xpath("normalize-space('hello world')") - expect(result).to eq('hello world') - end - - it "returns empty string for all whitespace" do - doc = parse('') - result = doc.xpath("normalize-space(' \t\n ')") - expect(result).to eq('') - end - - it "uses context node when no argument" do - xml = ' hello world ' - doc = parse(xml) - result = doc.xpath('normalize-space(//item)') - expect(result).to eq('hello world') - end - end - - describe "translate()" do - it "replaces matching characters" do - doc = parse('') - result = doc.xpath("translate('bar', 'abc', 'ABC')") - expect(result).to eq('BAr') - end - - it "removes characters when third arg is shorter" do - doc = parse('') - result = doc.xpath("translate('bar', 'abc', 'AB')") - expect(result).to eq('BAr') - end - - it "handles empty translation string" do - doc = parse('') - result = doc.xpath("translate('bar', 'abc', '')") - expect(result).to eq('r') - end - - it "works with numbers converted to strings" do - doc = parse('') - result = doc.xpath("translate('2', '123', 'abc')") - expect(result).to eq('b') - end - - it "handles characters not in search string" do - doc = parse('') - result = doc.xpath("translate('hello', 'eo', 'EO')") - expect(result).to eq('hEllO') - end - - it "is case sensitive" do - doc = parse('') - result = doc.xpath("translate('Hello', 'h', 'H')") - expect(result).to eq('Hello') # 'H' and 'h' are different - end - - it "works in predicates" do - xml = '' - doc = parse(xml) - # Normalize to uppercase and compare - result = doc.root.xpath('item[translate(@code, "abc", "ABC") = "ABC"]') - expect(result.size).to eq(2) - end - end - - describe "substring-before()" do - it "extracts substring before delimiter" do - doc = parse('') - result = doc.xpath("substring-before('1999/04/01', '/')") - expect(result).to eq('1999') - end - - it "returns empty string when delimiter not found" do - doc = parse('') - result = doc.xpath("substring-before('hello', 'x')") - expect(result).to eq('') - end - - it "returns empty string for empty delimiter" do - doc = parse('') - result = doc.xpath("substring-before('hello', '')") - expect(result).to eq('') - end - - it "handles delimiter at start" do - doc = parse('') - result = doc.xpath("substring-before('/path/to/file', '/')") - expect(result).to eq('') - end - - it "finds first occurrence only" do - doc = parse('') - result = doc.xpath("substring-before('a/b/c', '/')") - expect(result).to eq('a') - end - - it "works with attribute values" do - xml = '' - doc = parse(xml) - result = doc.xpath('substring-before(//item/@id, "@")') - expect(result).to eq('user') - end - - it "works in predicates" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('file[substring-before(@name, ".") = "doc"]') - expect(result.size).to eq(1) # Only doc.txt - end - end - - describe "substring-after()" do - it "extracts substring after delimiter" do - doc = parse('') - result = doc.xpath("substring-after('1999/04/01', '/')") - expect(result).to eq('04/01') - end - - it "returns empty string when delimiter not found" do - doc = parse('') - result = doc.xpath("substring-after('hello', 'x')") - expect(result).to eq('') - end - - it "returns entire string for empty delimiter" do - doc = parse('') - result = doc.xpath("substring-after('hello', '')") - expect(result).to eq('hello') - end - - it "handles delimiter at end" do - doc = parse('') - result = doc.xpath("substring-after('path/', '/')") - expect(result).to eq('') - end - - it "finds first occurrence only" do - doc = parse('') - result = doc.xpath("substring-after('a/b/c', '/')") - expect(result).to eq('b/c') - end - - it "works with attribute values" do - xml = '' - doc = parse(xml) - result = doc.xpath('substring-after(//item/@id, "@")') - expect(result).to eq('example.com') - end - - it "works in predicates" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('file[substring-after(@name, ".") = "txt"]') - expect(result.size).to eq(2) - end - end - - describe "string functions in predicates" do - it "uses starts-with in predicate" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('book[starts-with(@title, "XPath")]') - expect(result.size).to eq(2) - expect(result.map { |n| n[:id] }).to eq(['b1', 'b3']) - end - - it "uses contains in predicate" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('item[contains(@name, "pie")]') - expect(result.size).to eq(2) - end - - it "combines string functions" do - xml = ' test other' - doc = parse(xml) - # Find items where normalized text is 'test' - result = doc.root.xpath('item[normalize-space(.) = "test"]') - expect(result.size).to eq(1) - end - end - end - - describe "XPath boolean functions" do - describe "boolean()" do - it "converts number 1 to true" do - doc = parse('') - result = doc.xpath('boolean(1)') - expect(result).to eq(true) - end - - it "converts number 0 to false" do - doc = parse('') - result = doc.xpath('boolean(0)') - expect(result).to eq(false) - end - - it "converts NaN to false" do - doc = parse('') - result = doc.xpath('boolean(0 div 0)') - expect(result).to eq(false) - end - - it "converts positive number to true" do - doc = parse('') - result = doc.xpath('boolean(42)') - expect(result).to eq(true) - end - - it "converts negative number to true" do - doc = parse('') - result = doc.xpath('boolean(-5)') - expect(result).to eq(true) - end - - it "converts non-empty string to true" do - doc = parse('') - result = doc.xpath("boolean('text')") - expect(result).to eq(true) - end - - it "converts empty string to false" do - doc = parse('') - result = doc.xpath("boolean('')") - expect(result).to eq(false) - end - - it "converts non-empty nodeset to true" do - xml = '' - doc = parse(xml) - result = doc.xpath('boolean(//item)') - expect(result).to eq(true) - end - - it "converts empty nodeset to false" do - xml = '' - doc = parse(xml) - result = doc.xpath('boolean(//missing)') - expect(result).to eq(false) - end - - it "converts boolean true to true" do - doc = parse('') - result = doc.xpath('boolean(1 = 1)') - expect(result).to eq(true) - end - - it "converts boolean false to false" do - doc = parse('') - result = doc.xpath('boolean(1 = 2)') - expect(result).to eq(false) - end - - it "works in boolean operations" do - doc = parse('') - result = doc.xpath('boolean(1) and boolean(1)') - expect(result).to eq(true) - end - end - - describe "not()" do - it "negates true to false" do - doc = parse('') - result = doc.xpath('not(1 = 1)') - expect(result).to eq(false) - end - - it "negates false to true" do - doc = parse('') - result = doc.xpath('not(1 = 2)') - expect(result).to eq(true) - end - - it "works with true() function" do - doc = parse('') - result = doc.xpath('not(true())') - expect(result).to eq(false) - end - - it "works with false() function" do - doc = parse('') - result = doc.xpath('not(false())') - expect(result).to eq(true) - end - - it "converts non-zero number to false" do - doc = parse('') - result = doc.xpath('not(1)') - expect(result).to eq(false) - end - - it "converts zero to true" do - doc = parse('') - result = doc.xpath('not(0)') - expect(result).to eq(true) - end - - it "converts empty string to true" do - doc = parse('') - result = doc.xpath("not('')") - expect(result).to eq(true) - end - - it "converts non-empty string to false" do - doc = parse('') - result = doc.xpath("not('text')") - expect(result).to eq(false) - end - - it "works in predicates with attribute existence" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('*[not(@id)]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('b') - end - - it "works in predicates with child existence" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('section[not(item)]') - expect(result.size).to eq(1) - end - - it "double negation returns original" do - doc = parse('') - result = doc.xpath('not(not(true()))') - expect(result).to eq(true) - end - - it "works with nodeset conversion" do - xml = '' - doc = parse(xml) - result = doc.xpath('not(//missing)') - expect(result).to eq(true) - end - end - - describe "true() and false()" do - it "true() returns boolean true" do - doc = parse('') - result = doc.xpath('true()') - expect(result).to eq(true) - end - - it "false() returns boolean false" do - doc = parse('') - result = doc.xpath('false()') - expect(result).to eq(false) - end - - it "true() and true() returns true" do - doc = parse('') - result = doc.xpath('true() and true()') - expect(result).to eq(true) - end - - it "true() and false() returns false" do - doc = parse('') - result = doc.xpath('true() and false()') - expect(result).to eq(false) - end - - it "false() and false() returns false" do - doc = parse('') - result = doc.xpath('false() and false()') - expect(result).to eq(false) - end - - it "true() or false() returns true" do - doc = parse('') - result = doc.xpath('true() or false()') - expect(result).to eq(true) - end - - it "false() or false() returns false" do - doc = parse('') - result = doc.xpath('false() or false()') - expect(result).to eq(false) - end - - it "works in complex boolean expressions" do - doc = parse('') - result = doc.xpath('not(not(true()))') - expect(result).to eq(true) - end - - it "works in comparisons" do - doc = parse('') - result = doc.xpath('true() = true()') - expect(result).to eq(true) - end - - it "works in predicates" do - xml = '
' - doc = parse(xml) - # true() always matches, so all elements selected - result = doc.root.xpath('*[true()]') - expect(result.size).to eq(3) - end - - it "false() in predicate excludes all" do - xml = '' - doc = parse(xml) - # false() never matches, so no elements selected - result = doc.root.xpath('*[false()]') - expect(result).to be_empty - end - end - - describe "lang()" do - it "matches exact language code" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('lang("en")') - expect(result).to eq(true) - end - - it "returns false when language does not match" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('lang("fr")') - expect(result).to eq(false) - end - - it "matches language with region code" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('lang("en")') - expect(result).to eq(true) - end - - it "does not match region when base differs" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('lang("fr")') - expect(result).to eq(false) - end - - it "is case insensitive" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('lang("en")') - expect(result).to eq(true) - end - - it "inherits language from parent" do - xml = '
' - doc = parse(xml) - item = navigate_to(doc.root, 'section/item') - result = item.xpath('lang("fr")') - expect(result).to eq(true) - result_en = item.xpath('lang("en")') - expect(result_en).to eq(false) - end - - it "handles complex language inheritance" do - xml = '
' - doc = parse(xml) - item = navigate_to(doc.root, 'div/section/item') - result = item.xpath('lang("fr")') - expect(result).to eq(true) - end - end - - describe "boolean functions in complex expressions" do - it "combines boolean() and not()" do - doc = parse('') - result = doc.xpath('not(boolean(0))') - expect(result).to eq(true) - end - - it "uses boolean functions in conditional logic" do - xml = '' - doc = parse(xml) - # Select items with active attribute OR first position - result = doc.root.xpath('item[boolean(@active) or (position() = 1)]') - expect(result.size).to eq(1) - end - - it "uses not() with multiple conditions" do - xml = '
' - doc = parse(xml) - # Select elements without both id and class - result = doc.root.xpath('*[not(@id and @class)]') - expect(result.size).to eq(3) - expect(result.map(&:name)).to eq(['a', 'b', 'd']) - end - - it "uses boolean conversions in comparisons" do - doc = parse('') - result = doc.xpath('boolean(1) = true()') - expect(result).to eq(true) - end - - it "chains boolean operations" do - doc = parse('') - result = doc.xpath('true() and not(false()) and boolean(1)') - expect(result).to eq(true) - end - end - end - - describe "XPath node-set functions" do - describe "count()" do - it "counts nodes in a nodeset" do - xml = '' - doc = parse(xml) - result = doc.xpath('count(//item)') - expect(result).to eq(3) - end - - it "returns 0 for empty nodeset" do - xml = '' - doc = parse(xml) - result = doc.xpath('count(//missing)') - expect(result).to eq(0) - end - - it "works with predicates" do - xml = '' - doc = parse(xml) - result = doc.xpath('count(//item[@id])') - expect(result).to eq(2) - end - - it "works in comparison expressions" do - xml = '
' - doc = parse(xml) - # Select sections with more than 1 item - result = doc.root.xpath('section[count(item) > 1]') - expect(result.size).to eq(1) - expect(result.first.nodes.size).to eq(2) - end - - it "counts all descendants" do - xml = '
' - doc = parse(xml) - result = doc.xpath('count(//*)') - expect(result).to eq(5) # root, a, b, c, d - end - end - - describe "local-name()" do - it "returns local name of context node with no argument" do - xml = 'test' - doc = parse(xml) - result = doc.xpath('local-name(//item)') - expect(result).to eq('item') - end - - it "returns local name of first node in nodeset" do - xml = '' - doc = parse(xml) - result = doc.xpath('local-name(//*)') # First is root - expect(result).to eq('root') - end - - it "returns empty string for empty nodeset" do - xml = '' - doc = parse(xml) - result = doc.xpath('local-name(//missing)') - expect(result).to eq('') - end - - it "strips namespace prefix from qualified names" do - xml = '' - doc = parse(xml) - result = doc.xpath('local-name(//item)') - expect(result).to eq('item') - end - - it "works in predicates" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('*[local-name() = "item"]') - expect(result.size).to eq(2) - expect(result.map(&:name)).to all(eq('item')) - end - end - - describe "namespace-uri()" do - it "returns namespace URI of context node" do - xml = '' - doc = parse(xml) - result = doc.xpath('namespace-uri(//item)') - expect(result).to eq('http://example.org') - end - - it "returns empty string for node without namespace" do - xml = '' - doc = parse(xml) - result = doc.xpath('namespace-uri(//item)') - expect(result).to eq('') - end - - it "returns empty string for empty nodeset" do - xml = '' - doc = parse(xml) - result = doc.xpath('namespace-uri(//missing)') - expect(result).to eq('') - end - - it "works with prefixed namespaces" do - xml = '' - doc = parse(xml) - result = doc.xpath('namespace-uri(//item)') - expect(result).to eq('http://example.org') - end - - it "works in predicates" do - xml = '' - doc = parse(xml) - # Note: This test may need adjustment based on namespace handling - result = doc.root.xpath('*[namespace-uri() = "http://example.org"]') - expect(result.size).to be >= 0 - end - end - - describe "name()" do - it "returns qualified name of context node" do - xml = 'test' - doc = parse(xml) - result = doc.xpath('name(//item)') - expect(result).to eq('item') - end - - it "returns qualified name including prefix" do - xml = '' - doc = parse(xml) - # Note: Current implementation stores local name only, not prefix - result = doc.xpath('name(//item)') - expect(result).to eq('item') - end - - it "returns empty string for empty nodeset" do - xml = '' - doc = parse(xml) - result = doc.xpath('name(//missing)') - expect(result).to eq('') - end - - it "works in predicates" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('*[name() = "item"]') - expect(result.size).to eq(2) - end - - it "works with first node in nodeset" do - xml = '' - doc = parse(xml) - result = doc.xpath('name(//*)') # First element is root - expect(result).to eq('root') - end - end - - describe "id()" do - it "selects element by id attribute" do - xml = 'FoundNot' - doc = parse(xml) - result = doc.xpath('id("test")') - expect(result.size).to eq(1) - expect(result.first[:id]).to eq('test') - expect(result.first.text).to eq('Found') - end - - it "handles multiple space-separated ids" do - xml = 'ABC' - doc = parse(xml) - result = doc.xpath('id("a b")') - expect(result.size).to eq(2) - ids = result.map { |n| n[:id] } - expect(ids).to include('a', 'b') - end - - it "returns empty nodeset for non-existent id" do - xml = '' - doc = parse(xml) - result = doc.xpath('id("missing")') - expect(result.size).to eq(0) - end - - it "works with nodeset argument" do - xml = 'testFound' - doc = parse(xml) - # Get id value from ref element - result = doc.xpath('id(//ref)') - expect(result.size).to eq(1) - expect(result.first[:id]).to eq('test') - end - - it "ignores duplicate ids" do - xml = 'FirstSecond' - doc = parse(xml) - result = doc.xpath('id("test")') - # Should return both elements with id="test" - expect(result.size).to be > 1 - end - - it "works in complex expressions" do - xml = 'abAB' - doc = parse(xml) - result = doc.xpath('id(//ref)') - expect(result.size).to eq(2) - end - end - - describe "node-set functions in combination" do - it "combines count() with predicates" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('section[count(item) = 2]') - expect(result.size).to eq(1) - end - - it "uses name() to filter elements" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('*[name() = "item" or name() = "book"]') - expect(result.size).to eq(3) - end - - it "combines local-name() with count()" do - xml = '' - doc = parse(xml) - result = doc.xpath('count(//*[local-name() = "item"])') - expect(result).to eq(3) - end - end - end -end \ No newline at end of file diff --git a/spec/taurus/errors_spec.rb b/spec/taurus/errors_spec.rb deleted file mode 100644 index b9f3de3..0000000 --- a/spec/taurus/errors_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe "Error Infrastructure" do - describe "Error Code Mapping" do - it "maps all parse error codes correctly" do - expect(Taurus::FFI::ErrorCode::OK).to eq(0) - expect(Taurus::FFI::ErrorCode::NULL_INPUT).to eq(1) - expect(Taurus::FFI::ErrorCode::UNCLOSED_TAG).to eq(100) - expect(Taurus::FFI::ErrorCode::INVALID_ATTR).to eq(101) - end - - it "maps all XPath error codes correctly" do - expect(Taurus::FFI::ErrorCode::XPATH_SYNTAX).to eq(200) - expect(Taurus::FFI::ErrorCode::XPATH_FUNCTION).to eq(201) - expect(Taurus::FFI::ErrorCode::XPATH_TYPE_MISMATCH).to eq(202) - end - - it "maps all evaluation error codes correctly" do - expect(Taurus::FFI::ErrorCode::EVAL_CONTEXT).to eq(300) - expect(Taurus::FFI::ErrorCode::EVAL_ARGUMENT).to eq(301) - expect(Taurus::FFI::ErrorCode::EVAL_OVERFLOW).to eq(302) - end - - it "maps generic error codes correctly" do - expect(Taurus::FFI::ErrorCode::OUT_OF_MEMORY).to eq(900) - expect(Taurus::FFI::ErrorCode::INTERNAL).to eq(999) - end - end - - describe "Error Code to Symbol Conversion" do - it "converts parse error codes to symbols" do - expect(Taurus::FFI.error_code_to_sym(Taurus::FFI::ErrorCode::OK)).to eq(:ok) - expect(Taurus::FFI.error_code_to_sym(Taurus::FFI::ErrorCode::UNCLOSED_TAG)).to eq(:unclosed_tag) - expect(Taurus::FFI.error_code_to_sym(Taurus::FFI::ErrorCode::MALFORMED)).to eq(:malformed) - end - - it "converts XPath error codes to symbols" do - expect(Taurus::FFI.error_code_to_sym(Taurus::FFI::ErrorCode::XPATH_SYNTAX)).to eq(:xpath_syntax) - expect(Taurus::FFI.error_code_to_sym(Taurus::FFI::ErrorCode::XPATH_NAMESPACE)).to eq(:xpath_namespace) - end - - it "converts evaluation error codes to symbols" do - expect(Taurus::FFI.error_code_to_sym(Taurus::FFI::ErrorCode::EVAL_ARGUMENT)).to eq(:eval_argument) - end - - it "handles unknown error codes" do - expect(Taurus::FFI.error_code_to_sym(9999)).to eq(:unknown) - end - end - - describe "Error Class Hierarchy" do - it "defines Error as base class" do - expect(Taurus::Error.superclass).to eq(StandardError) - end - - it "defines ParseError inheriting from Error" do - expect(Taurus::ParseError.superclass).to eq(Taurus::Error) - end - - it "defines XPathError inheriting from Error" do - expect(Taurus::XPathError.superclass).to eq(Taurus::Error) - end - - it "defines EvaluationError inheriting from Error" do - expect(Taurus::EvaluationError.superclass).to eq(Taurus::Error) - end - end - - describe "Error Infrastructure Ready" do - it "has context snippet API" do - expect(Taurus::FFI).to respond_to(:taurus_error_context) - expect(Taurus::FFI).to respond_to(:taurus_error_byte_offset) - end - - it "has error setting functions available in C" do - # These are internal functions used by parser/evaluator - # They're declared in taurus_internal.h: - # - taurus_set_error - # - taurus_set_error_with_context - # - taurus_extract_context_snippet - # Verified by successful compilation - expect(true).to be true - end - end - - describe "FFI Error Functions" do - it "has taurus_error_context binding" do - expect(Taurus::FFI).to respond_to(:taurus_error_context) - end - - it "has taurus_error_byte_offset binding" do - expect(Taurus::FFI).to respond_to(:taurus_error_byte_offset) - end - - it "has all error functions bound" do - expect(Taurus::FFI).to respond_to(:taurus_last_error) - expect(Taurus::FFI).to respond_to(:taurus_last_error_code) - expect(Taurus::FFI).to respond_to(:taurus_error_string) - expect(Taurus::FFI).to respond_to(:taurus_clear_error) - expect(Taurus::FFI).to respond_to(:taurus_parse_error_line) - expect(Taurus::FFI).to respond_to(:taurus_parse_error_column) - end - end -end \ No newline at end of file diff --git a/spec/taurus/namespace_spec.rb b/spec/taurus/namespace_spec.rb deleted file mode 100644 index 28072b7..0000000 --- a/spec/taurus/namespace_spec.rb +++ /dev/null @@ -1,224 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe Taurus::Element, "namespace support" do - describe "#namespace" do - it "returns nil when element has no namespace" do - elem = Taurus::Element.new("item") - expect(elem.namespace).to be_nil - end - - it "returns namespace hash when element has a namespace" do - elem = Taurus::Element.new("ex:item") - # In a real implementation with parser, this would be set during parsing - # For now, we're testing the API structure - - # The namespace method returns { prefix: "ex", href: "http://example.org" } - # when namespaces are properly tracked - end - end - - describe "#namespaces" do - it "returns empty array when element has no namespace declarations" do - elem = Taurus::Element.new("item") - expect(elem.namespaces).to eq([]) - end - - it "returns array of namespace declarations" do - elem = Taurus::Element.new("root") - # Namespace declarations would be added during parsing - # expect(elem.namespaces).to be_an(Array) - end - end - - describe "#namespace_for_prefix" do - it "returns nil when prefix is not found" do - elem = Taurus::Element.new("item") - expect(elem.namespace_for_prefix("ex")).to be_nil - end - - it "resolves default namespace with nil prefix" do - elem = Taurus::Element.new("item") - # Would return URI if default namespace exists - expect(elem.namespace_for_prefix(nil)).to be_nil - end - end - - describe "#parent=" do - it "sets parent to nil" do - elem = Taurus::Element.new("item") - elem.parent = nil - expect(elem.parent).to be_nil - end - - it "sets parent to another element" do - parent = Taurus::Element.new("root") - child = Taurus::Element.new("item") - - child.parent = parent - expect(child.parent).to eq(parent) - end - - it "raises error when setting self as parent" do - elem = Taurus::Element.new("item") - expect { elem.parent = elem }.to raise_error(ArgumentError, /own parent/) - end - - it "raises error when parent is not an Element" do - elem = Taurus::Element.new("item") - expect { elem.parent = "not an element" }.to raise_error(TypeError) - end - end - - describe "convenience methods" do - describe "#<<" do - it "adds a child node" do - elem = Taurus::Element.new("root") - child = Taurus::Element.new("item") - - elem << child - expect(elem.nodes).to include(child) - end - - it "returns self for chaining" do - elem = Taurus::Element.new("root") - child = Taurus::Element.new("item") - - result = elem << child - expect(result).to eq(elem) - end - end - - describe "#remove" do - it "removes element from parent" do - parent = Taurus::Element.new("root") - child = Taurus::Element.new("item") - - parent << child - child.parent = parent - - child.remove - expect(parent.nodes).not_to include(child) - expect(child.parent).to be_nil - end - - it "does nothing when element has no parent" do - elem = Taurus::Element.new("item") - expect { elem.remove }.not_to raise_error - end - end - - describe "#add_child" do - it "adds child and sets parent relationship" do - parent = Taurus::Element.new("root") - child = Taurus::Element.new("item") - - parent.add_child(child) - expect(parent.nodes).to include(child) - expect(child.parent).to eq(parent) - end - - it "removes child from old parent before adding" do - old_parent = Taurus::Element.new("old") - new_parent = Taurus::Element.new("new") - child = Taurus::Element.new("item") - - old_parent.add_child(child) - new_parent.add_child(child) - - expect(old_parent.nodes).not_to include(child) - expect(new_parent.nodes).to include(child) - expect(child.parent).to eq(new_parent) - end - end - - describe "#text" do - it "returns first text node" do - elem = Taurus::Element.new("item") - elem << "Hello" - elem << "World" - - expect(elem.text).to eq("Hello") - end - - it "returns nil when no text nodes" do - elem = Taurus::Element.new("item") - expect(elem.text).to be_nil - end - end - - describe "#replace_text" do - it "replaces all child nodes with text" do - elem = Taurus::Element.new("item") - elem << Taurus::Element.new("child") - elem << "old text" - - elem.replace_text("new text") - expect(elem.nodes).to eq(["new text"]) - end - - it "raises error for non-string argument" do - elem = Taurus::Element.new("item") - expect { elem.replace_text(123) }.to raise_error(ArgumentError) - end - end - - describe "#[] and #[]=" do - it "gets and sets attributes" do - elem = Taurus::Element.new("item") - elem["id"] = "123" - - expect(elem["id"]).to eq("123") - end - - it "handles symbol keys" do - elem = Taurus::Element.new("item") - elem[:id] = "123" - - expect(elem[:id]).to eq("123") - end - end - - describe "#namespace_prefix" do - it "returns nil when no namespace" do - elem = Taurus::Element.new("item") - expect(elem.namespace_prefix).to be_nil - end - end - - describe "#namespace_uri" do - it "returns nil when no namespace" do - elem = Taurus::Element.new("item") - expect(elem.namespace_uri).to be_nil - end - end - - describe "#namespace?" do - it "returns false when no namespace" do - elem = Taurus::Element.new("item") - expect(elem.namespace?).to be false - end - end - - describe "#all_namespaces" do - it "returns empty hash when no namespaces" do - elem = Taurus::Element.new("item") - expect(elem.all_namespaces).to eq({}) - end - end - - describe ".with_namespace" do - it "creates element with namespace" do - elem = Taurus::Element.with_namespace("item", prefix: "ex", href: "http://example.org") - expect(elem.name).to eq("item") - expect(elem.attributes["xmlns:ex"]).to eq("http://example.org") - end - - it "creates element with default namespace" do - elem = Taurus::Element.with_namespace("item", href: "http://example.org") - expect(elem.attributes["xmlns"]).to eq("http://example.org") - end - end - end -end \ No newline at end of file diff --git a/spec/taurus/ox_compatibility_spec.rb b/spec/taurus/ox_compatibility_spec.rb deleted file mode 100644 index 35d718a..0000000 --- a/spec/taurus/ox_compatibility_spec.rb +++ /dev/null @@ -1,175 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe "Ox Compatibility" do - describe "Basic parsing" do - it "parses simple XML" do - xml = "test" - doc = Taurus.parse(xml) - - expect(doc).to be_a(Taurus::Document) - expect(doc.root.name).to eq("top") - expect(doc.root.text).to eq("test") - end - - it "parses XML with attributes" do - xml = 'test' - doc = Taurus.parse(xml) - - expect(doc.root["name"]).to eq("Pete") - end - - it "parses nested elements" do - xml = 'value' - doc = Taurus.parse(xml) - - expect(doc.root.nodes.size).to eq(1) - expect(doc.root.nodes[0].name).to eq("nested") - expect(doc.root.nodes[0].text).to eq("value") - end - end - - describe "Namespace support" do - it "parses default namespace declaration" do - xml = 'content' - doc = Taurus.parse(xml) - - expect(doc.root.namespace).not_to be_nil - expect(doc.root.namespace[:href]).to eq("http://example.org") - expect(doc.root.namespace[:prefix]).to be_nil - end - - it "parses prefixed namespace declaration" do - xml = 'content' - doc = Taurus.parse(xml) - - expect(doc.root.namespaces.size).to eq(1) - ns = doc.root.namespaces[0] - expect(ns[:prefix]).to eq("ex") - expect(ns[:href]).to eq("http://example.org") - end - - it "parses multiple namespace declarations" do - xml = 'content' - doc = Taurus.parse(xml) - - expect(doc.root.namespaces.size).to eq(3) - end - - it "parses namespaced elements" do - xml = 'test' - doc = Taurus.parse(xml) - - item = doc.root.nodes[0] - expect(item.name).to eq("item") - expect(item.namespace).not_to be_nil - expect(item.namespace[:prefix]).to eq("ex") - expect(item.namespace[:href]).to eq("http://example.org") - end - - it "parses namespaced attributes" do - xml = 'content' - doc = Taurus.parse(xml) - - # Namespaced attributes are stored in the attributes hash - expect(doc.root["ex:name"]).to eq("value") - end - end - - describe "Namespace inheritance" do - it "inherits default namespace to child elements" do - xml = 'content' - doc = Taurus.parse(xml) - - child = doc.root.nodes[0] - expect(child.namespace).not_to be_nil - expect(child.namespace[:href]).to eq("http://example.org") - end - - it "inherits prefixed namespace to child elements" do - xml = 'test' - doc = Taurus.parse(xml) - - item = doc.root.nodes[0].nodes[0] - expect(item.namespace).not_to be_nil - expect(item.namespace[:prefix]).to eq("ex") - expect(item.namespace[:href]).to eq("http://example.org") - end - - it "allows namespace redeclaration in child elements" do - xml = 'test' - doc = Taurus.parse(xml) - - item = doc.root.nodes[0].nodes[0] - expect(item.namespace).not_to be_nil - expect(item.namespace[:prefix]).to eq("ex") - expect(item.namespace[:href]).to eq("http://other.org") - end - end - - describe "Namespace resolution" do - it "resolves prefixes using namespace_for_prefix" do - xml = 'test' - doc = Taurus.parse(xml) - - href = doc.root.namespace_for_prefix("ex") - expect(href).to eq("http://example.org") - end - - it "resolves default namespace with nil prefix" do - xml = 'content' - doc = Taurus.parse(xml) - - href = doc.root.namespace_for_prefix(nil) - expect(href).to eq("http://default.org") - end - - it "resolves inherited namespaces" do - xml = 'test' - doc = Taurus.parse(xml) - - href = doc.root.nodes[0].namespace_for_prefix("ex") - expect(href).to eq("http://example.org") - end - end - - describe "Special XML features" do - it "parses comments" do - xml = 'content' - doc = Taurus.parse(xml) - - # Comments are stored as nodes - expect(doc.root.nodes.size).to eq(2) - expect(doc.root.nodes[0]).to be_a(String) # The comment text - expect(doc.root.nodes[1]).to eq("content") # The text content - end - - it "parses CDATA sections" do - xml = 'content]]>' - doc = Taurus.parse(xml) - - expect(doc.root.text).to eq("content") - end - - it "parses self-closing elements" do - xml = '' - doc = Taurus.parse(xml) - - expect(doc.root.nodes.size).to eq(1) - expect(doc.root.nodes[0].name).to eq("item") - end - end - - describe "Error handling" do - it "handles malformed XML gracefully" do - xml = 'test' # Mismatched tag - expect { Taurus.parse(xml) }.to raise_error(Taurus::ParseError) - end - - it "handles mismatched tags" do - xml = 'test' # Missing closing tag for nested - expect { Taurus.parse(xml) }.to raise_error(Taurus::ParseError) - end - end -end \ No newline at end of file diff --git a/spec/taurus/parse_errors_spec.rb b/spec/taurus/parse_errors_spec.rb deleted file mode 100644 index c373bc1..0000000 --- a/spec/taurus/parse_errors_spec.rb +++ /dev/null @@ -1,218 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe "Parse Error Messages" do - describe "NULL Input Errors" do - it "reports NULL input with clear error" do - expect { - Taurus.parse(nil) - }.to raise_error(Taurus::ParseError) do |error| - expect(error.message).to include("NULL input") - expect(error.code).to eq(:null_input) - end - end - end - - describe "Empty Input Errors" do - it "reports empty input with clear error" do - expect { - Taurus.parse("") - }.to raise_error(Taurus::ParseError) do |error| - expect(error.message).to include("Empty input") - expect(error.code).to eq(:empty_input) - end - end - end - - describe "Invalid XML Structure" do - it "reports missing root element" do - xml = " \n\n " - - expect { - Taurus.parse(xml) - }.to raise_error(Taurus::ParseError) do |error| - expect(error.message).to include("Expected root element") - expect(error.code).to eq(:invalid_xml) - expect(error.line).to be > 0 - expect(error.column).to be > 0 - end - end - - it "reports text before root element" do - xml = "Some text " - - expect { - Taurus.parse(xml) - }.to raise_error(Taurus::ParseError) do |error| - expect(error.message).to include("Expected root element") - expect(error.code).to eq(:invalid_xml) - end - end - end - - describe "Parse Failed Errors" do - it "reports failed root element parse" do - xml = "<>" - - expect { - Taurus.parse(xml) - }.to raise_error(Taurus::ParseError) do |error| - expect(error.message).to include("Failed to parse") - expect(error.code).to eq(:parse_failed) - expect(error.line).to eq(1) - expect(error.column).to be > 0 - end - end - - it "reports malformed element name" do - xml = "<123invalid>" - - expect { - Taurus.parse(xml) - }.to raise_error(Taurus::ParseError) do |error| - expect(error.code).to eq(:parse_failed) - end - end - end - - describe "Error Context" do - it "provides context snippet for parse errors" do - xml = <<~XML - - - - This is some text - - - Extra text here - XML - - # This should fail because of text after root element close - # But our lenient parser might accept it - test what we can - result = Taurus.parse(xml) - expect(result).to be_a(Taurus::Document) - expect(result.root.name).to eq("library") - end - - it "shows line and column for errors" do - xml = "<" - - expect { - Taurus.parse(xml) - }.to raise_error(Taurus::ParseError) do |error| - expect(error.line).to eq(1) - expect(error.column).to eq(1) - expect(error.byte_offset).to eq(0) - end - end - - it "tracks position through whitespace and newlines" do - xml = <<~XML - - - - XML - - # Should parse successfully - doc = Taurus.parse(xml) - expect(doc.root.name).to eq("root") - end - end - - describe "Error Information Completeness" do - it "includes all error details" do - xml = "" - - expect { - Taurus.parse(xml) - }.to raise_error(Taurus::ParseError) do |error| - # Check error has all expected attributes - expect(error).to respond_to(:message) - expect(error).to respond_to(:code) - expect(error).to respond_to(:line) - expect(error).to respond_to(:column) - expect(error).to respond_to(:byte_offset) - expect(error).to respond_to(:context) - - # Verify values are reasonable - expect(error.message).to be_a(String) - expect(error.message).not_to be_empty - expect(error.code).to be_a(Symbol) - end - end - end - - describe "Multi-line Error Context" do - it "handles errors on different lines" do - xml = <<~XML - - - - - - XML - - # Should parse successfully - doc = Taurus.parse(xml) - expect(doc.root.name).to eq("root") - expect(doc.root.nodes.length).to eq(2) - end - end - - describe "Error Recovery" do - it "clears previous errors on successful parse" do - # First, cause an error - expect { - Taurus.parse(nil) - }.to raise_error(Taurus::ParseError) - - # Then parse successfully - doc = Taurus.parse("") - expect(doc).to be_a(Taurus::Document) - expect(doc.root.name).to eq("root") - - # Verify no error state lingering - expect(Taurus::FFI.taurus_last_error).to be_nil - end - end - - describe "Position Tracking Accuracy" do - # NOTE: These tests hang in RSpec but work in plain Ruby - # TODO: Investigate RSpec-specific issue (possibly threading/FD related) - - xit "tracks position through XML declaration" do - xml = <<~XML - - - XML - - doc = Taurus.parse(xml) - expect(doc.root.name).to eq("root") - end - - xit "tracks position through comments" do - xml = <<~XML - - - - - - XML - - doc = Taurus.parse(xml) - expect(doc.root.name).to eq("root") - end - - xit "tracks position through processing instructions" do - xml = <<~XML - - - - XML - - doc = Taurus.parse(xml) - expect(doc.root.name).to eq("root") - end - end -end \ No newline at end of file diff --git a/spec/taurus/parser_spec.rb b/spec/taurus/parser_spec.rb deleted file mode 100644 index 388cfb0..0000000 --- a/spec/taurus/parser_spec.rb +++ /dev/null @@ -1,296 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe "Taurus XML Parser with Namespace Support" do - describe "basic XML parsing" do - it "parses simple XML without namespaces" do - xml = 'content' - doc = Taurus.parse(xml) - - expect(doc).to be_a(Taurus::Document) - expect(doc.root).to be_a(Taurus::Element) - expect(doc.root.name).to eq("root") - expect(doc.root.nodes.size).to eq(1) - expect(doc.root.nodes.first.name).to eq("item") - expect(doc.root.nodes.first.text).to eq("content") - end - - it "parses XML with attributes" do - xml = 'text' - doc = Taurus.parse(xml) - - expect(doc.root.attributes["id"]).to eq("123") - expect(doc.root.nodes.first.attributes["attr"]).to eq("value") - expect(doc.root.nodes.first.text).to eq("text") - end - - it "parses nested elements" do - xml = 'deep' - doc = Taurus.parse(xml) - - parent = doc.root.nodes.first - expect(parent.name).to eq("parent") - expect(parent.nodes.first.name).to eq("child") - expect(parent.nodes.first.text).to eq("deep") - end - end - - describe "namespace parsing" do - it "parses default namespace declaration" do - xml = 'content' - doc = Taurus.parse(xml) - - expect(doc.root.namespace_uri).to eq("http://example.org") - expect(doc.root.namespace_prefix).to be_nil - end - - it "parses prefixed namespace declaration" do - xml = 'content' - doc = Taurus.parse(xml) - - expect(doc.root.namespaces.size).to eq(1) - expect(doc.root.namespaces.first[:prefix]).to eq("ex") - expect(doc.root.namespaces.first[:href]).to eq("http://example.org") - end - - it "parses multiple namespace declarations" do - xml = '' - doc = Taurus.parse(xml) - - expect(doc.root.namespaces.size).to eq(3) - - # Find each namespace - default_ns = doc.root.namespaces.find { |ns| ns[:prefix].nil? } - ex_ns = doc.root.namespaces.find { |ns| ns[:prefix] == "ex" } - other_ns = doc.root.namespaces.find { |ns| ns[:prefix] == "other" } - - expect(default_ns[:href]).to eq("http://default.org") - expect(ex_ns[:href]).to eq("http://example.org") - expect(other_ns[:href]).to eq("http://other.org") - end - - it "parses namespaced elements" do - xml = 'content' - doc = Taurus.parse(xml) - - item = doc.root.nodes.first - expect(item.name).to eq("item") # Local name only (Ox compatibility) - expect(item.qualified_name).to eq("ex:item") # Full qualified name with prefix - expect(item.namespace_prefix).to eq("ex") - expect(item.namespace_uri).to eq("http://example.org") - end - - it "parses namespaced attributes" do - xml = 'content' - doc = Taurus.parse(xml) - - item = doc.root.nodes.first - expect(item.attributes["ex:attr"]).to eq("value") - end - end - - describe "namespace inheritance" do - it "inherits default namespace to child elements" do - xml = 'content' - doc = Taurus.parse(xml) - - item = doc.root.nodes.first - expect(item.namespace_uri).to eq("http://example.org") - expect(item.namespace_prefix).to be_nil - end - - it "inherits prefixed namespace to child elements" do - xml = 'content' - doc = Taurus.parse(xml) - - item = doc.root.nodes.first - child = item.nodes.first - - expect(item.namespace_uri).to eq("http://example.org") - expect(child.namespace_uri).to eq("http://example.org") - end - - it "allows namespace redeclaration in child elements" do - xml = 'content' - doc = Taurus.parse(xml) - - expect(doc.root.namespace_uri).to eq("http://default.org") - expect(doc.root.nodes.first.namespace_uri).to eq("http://new.org") - end - end - - describe "namespace resolution" do - it "resolves prefixes using namespace_for_prefix" do - xml = '' - doc = Taurus.parse(xml) - - expect(doc.root.namespace_for_prefix("ex")).to eq("http://example.org") - expect(doc.root.namespace_for_prefix("other")).to eq("http://other.org") - expect(doc.root.namespace_for_prefix("nonexistent")).to be_nil - end - - it "resolves default namespace with nil prefix" do - xml = '' - doc = Taurus.parse(xml) - - expect(doc.root.namespace_for_prefix(nil)).to eq("http://default.org") - end - - it "resolves inherited namespaces" do - xml = '' - doc = Taurus.parse(xml) - - child = doc.root.nodes.first.nodes.first - expect(child.namespace_for_prefix("ex")).to eq("http://example.org") - end - end - - describe "complex namespace scenarios" do - it "handles mixed default and prefixed namespaces" do - xml = <<~XML - - default - prefixed - - XML - - doc = Taurus.parse(xml.strip) - - default_item = doc.root.nodes[0] - prefixed_item = doc.root.nodes[1] - - expect(default_item.namespace_uri).to eq("http://default.org") - expect(prefixed_item.namespace_uri).to eq("http://example.org") - expect(prefixed_item.namespace_prefix).to eq("ex") - end - - it "handles deeply nested namespaces" do - xml = <<~XML - - - - content - - - - XML - - doc = Taurus.parse(xml.strip) - - level1 = doc.root.nodes.first - level2 = level1.nodes.first - level3 = level2.nodes.first - - expect(level1.namespace_uri).to eq("http://root.org") - expect(level2.namespace_uri).to eq("http://root.org") - expect(level3.namespace_uri).to eq("http://root.org") - - expect(level1.namespace_for_prefix("a")).to eq("http://a.org") - expect(level1.namespace_for_prefix("b")).to eq("http://b.org") - expect(level2.namespace_for_prefix("c")).to eq("http://c.org") - end - end - - describe "error handling" do - it "handles malformed XML gracefully" do - xml = 'unclosed' - - expect { Taurus.parse(xml) }.to raise_error(Taurus::ParseError) - end - - it "handles mismatched tags" do - xml = '' - - expect { Taurus.parse(xml) }.to raise_error(Taurus::ParseError) - end - - it "handles invalid characters in names" do - xml = '<123invalid>content' - - expect { Taurus.parse(xml) }.to raise_error(Taurus::ParseError) - end - end - - describe "special XML features" do - it "parses comments" do - xml = 'content' - doc = Taurus.parse(xml) - - expect(doc.root.nodes.size).to eq(2) # Comment text + element - expect(doc.root.nodes[0]).to be_a(String) # The comment text - expect(doc.root.nodes[1].name).to eq("item") - end - - it "parses CDATA sections" do - xml = 'content]]>' - doc = Taurus.parse(xml) - - expect(doc.root.nodes.first.text).to include("content") - end - - it "parses self-closing elements" do - xml = '' - doc = Taurus.parse(xml) - - expect(doc.root.nodes.size).to eq(1) - expect(doc.root.nodes.first.name).to eq("item") - end - end - - describe "file parsing" do - it "parses XML from file" do - require 'tempfile' - - xml = 'content' - - Tempfile.create(['test', '.xml']) do |file| - file.write(xml) - file.close - - doc = Taurus.parse_file(file.path) - expect(doc.root.name).to eq("root") - expect(doc.root.nodes.first.name).to eq("item") - end - end - - it "handles missing file gracefully" do - expect { Taurus.parse_file("/nonexistent/file.xml") }.to raise_error(Errno::ENOENT) - end - end - - describe "all_namespaces method" do - it "returns all namespaces including inherited" do - xml = <<~XML - - - - - - XML - - doc = Taurus.parse(xml.strip) - - root = doc.root - child = root.nodes.first - grandchild = child.nodes.first - - expect(root.all_namespaces).to eq({ - nil => "http://root.org", - "a" => "http://a.org" - }) - - expect(child.all_namespaces).to eq({ - nil => "http://root.org", - "a" => "http://a.org", - "b" => "http://b.org" - }) - - expect(grandchild.all_namespaces).to eq({ - nil => "http://root.org", - "a" => "http://a.org", - "b" => "http://b.org" - }) - end - end -end \ No newline at end of file diff --git a/spec/taurus/xpath/lexer_spec.rb b/spec/taurus/xpath/lexer_spec.rb deleted file mode 100644 index f594607..0000000 --- a/spec/taurus/xpath/lexer_spec.rb +++ /dev/null @@ -1,256 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe "Taurus::XPath.tokenize" do - describe "basic tokenization" do - it "tokenizes simple path expressions" do - tokens = Taurus::XPath.tokenize("/root/child") - - expect(tokens.size).to eq(4) - expect(tokens[0]).to include("type" => "SLASH", "value" => "/") - expect(tokens[1]).to include("type" => "NCNAME", "value" => "root") - expect(tokens[2]).to include("type" => "SLASH", "value" => "/") - expect(tokens[3]).to include("type" => "NCNAME", "value" => "child") - end - - it "tokenizes descendant-or-self axis" do - tokens = Taurus::XPath.tokenize("//child") - - expect(tokens.size).to eq(2) - expect(tokens[0]).to include("type" => "DOUBLE_SLASH", "value" => "//") - expect(tokens[1]).to include("type" => "NCNAME", "value" => "child") - end - - it "tokenizes attribute access" do - tokens = Taurus::XPath.tokenize("/root/@attr") - - expect(tokens.size).to eq(5) - expect(tokens[0]).to include("type" => "SLASH", "value" => "/") - expect(tokens[1]).to include("type" => "NCNAME", "value" => "root") - expect(tokens[2]).to include("type" => "SLASH", "value" => "/") - expect(tokens[3]).to include("type" => "AT", "value" => "@") - expect(tokens[4]).to include("type" => "NCNAME", "value" => "attr") - end - - it "tokenizes self and parent references" do - tokens = Taurus::XPath.tokenize("./..") - - expect(tokens.size).to eq(3) - expect(tokens[0]).to include("type" => "DOT", "value" => ".") - expect(tokens[1]).to include("type" => "SLASH", "value" => "/") - expect(tokens[2]).to include("type" => "DOUBLE_DOT", "value" => "..") - end - end - - describe "operators" do - it "tokenizes comparison operators" do - tokens = Taurus::XPath.tokenize("a = b != c < d <= e > f >= g") - - expect(tokens.size).to eq(13) - expect(tokens[0]).to include("type" => "NCNAME", "value" => "a") - expect(tokens[2]).to include("type" => "NCNAME", "value" => "b") - expect(tokens[3]).to include("type" => "NOT_EQUALS", "value" => "!=") - expect(tokens[4]).to include("type" => "NCNAME", "value" => "c") - expect(tokens[5]).to include("type" => "LT", "value" => "<") - expect(tokens[7]).to include("type" => "LE", "value" => "<=") - expect(tokens[6]).to include("type" => "NCNAME", "value" => "d") - end - - it "tokenizes arithmetic operators" do - tokens = Taurus::XPath.tokenize("a + b - c * d div e mod f") - - expect(tokens.size).to eq(11) - expect(tokens[0]).to include("type" => "NCNAME", "value" => "a") - expect(tokens[1]).to include("type" => "PLUS", "value" => "+") - expect(tokens[2]).to include("type" => "NCNAME", "value" => "b") - expect(tokens[3]).to include("type" => "MINUS", "value" => "-") - expect(tokens[4]).to include("type" => "NCNAME", "value" => "c") - expect(tokens[5]).to include("type" => "STAR", "value" => "*") - expect(tokens[6]).to include("type" => "NCNAME", "value" => "d") - expect(tokens[7]).to include("type" => "DIV", "value" => "div") - expect(tokens[8]).to include("type" => "NCNAME", "value" => "e") - expect(tokens[9]).to include("type" => "MOD", "value" => "mod") - expect(tokens[10]).to include("type" => "NCNAME", "value" => "f") - end - - it "tokenizes logical operators" do - tokens = Taurus::XPath.tokenize("a and b or c") - - expect(tokens.size).to eq(5) - expect(tokens[1]).to include("type" => "AND", "value" => "and") - expect(tokens[3]).to include("type" => "OR", "value" => "or") - end - end - - describe "literals" do - it "tokenizes numbers" do - tokens = Taurus::XPath.tokenize("123 45.67") - - expect(tokens.size).to eq(2) - expect(tokens[0]).to include("type" => "NUMBER", "value" => "123") - expect(tokens[1]).to include("type" => "NUMBER", "value" => "45.67") - end - - it "tokenizes strings" do - tokens = Taurus::XPath.tokenize("'hello' \"world\"") - - expect(tokens.size).to eq(2) - expect(tokens[0]).to include("type" => "STRING", "value" => "'hello'") - expect(tokens[1]).to include("type" => "STRING", "value" => "\"world\"") - end - end - - describe "names and qnames" do - it "tokenizes NCNames" do - tokens = Taurus::XPath.tokenize("root child _private") - - expect(tokens.size).to eq(3) - expect(tokens[0]).to include("type" => "NCNAME", "value" => "root") - expect(tokens[1]).to include("type" => "NCNAME", "value" => "child") - expect(tokens[2]).to include("type" => "NCNAME", "value" => "_private") - end - - it "tokenizes QNames" do - tokens = Taurus::XPath.tokenize("ns:root prefix:child") - - expect(tokens.size).to eq(2) - expect(tokens[0]).to include("type" => "QNAME", "value" => "ns:root") - expect(tokens[1]).to include("type" => "QNAME", "value" => "prefix:child") - end - end - - describe "axes" do - it "tokenizes axis names" do - tokens = Taurus::XPath.tokenize("ancestor:: descendant-or-self:: following-sibling::") - - expect(tokens.size).to eq(6) - expect(tokens[0]).to include("type" => "ANCESTOR", "value" => "ancestor") - expect(tokens[1]).to include("type" => "DOUBLE_COLON", "value" => "::") - expect(tokens[2]).to include("type" => "DESCENDANT_OR_SELF", "value" => "descendant-or-self") - expect(tokens[3]).to include("type" => "DOUBLE_COLON", "value" => "::") - expect(tokens[4]).to include("type" => "FOLLOWING_SIBLING", "value" => "following-sibling") - expect(tokens[5]).to include("type" => "DOUBLE_COLON", "value" => "::") - end - - it "tokenizes all axis types" do - xpath = "ancestor:: ancestor-or-self:: attribute:: child:: descendant:: descendant-or-self:: following:: following-sibling:: namespace:: parent:: preceding:: preceding-sibling:: self::" - tokens = Taurus::XPath.tokenize(xpath) - - axis_types = tokens.select { |t| t["type"].end_with?("::") }.map { |t| t["type"] } - expect(axis_types).to be_empty # Should be DOUBLE_COLON - - axis_names = tokens.select { |t| xpath_token_is_axis_name?(t["type"]) } - expected_axes = %w[ANCESTOR ANCESTOR_OR_SELF ATTRIBUTE CHILD DESCENDANT DESCENDANT_OR_SELF FOLLOWING FOLLOWING_SIBLING NAMESPACE PARENT PRECEDING PRECEDING_SIBLING SELF] - expect(axis_names.map { |t| t["type"] }).to eq(expected_axes) - end - end - - describe "node types" do - it "tokenizes node type tests" do - tokens = Taurus::XPath.tokenize("comment() text() processing-instruction() node()") - - expect(tokens.size).to eq(12) - expect(tokens[0]).to include("type" => "COMMENT", "value" => "comment") - expect(tokens[1]).to include("type" => "LPAREN", "value" => "(") - expect(tokens[2]).to include("type" => "RPAREN", "value" => ")") - expect(tokens[3]).to include("type" => "TEXT", "value" => "text") - expect(tokens[4]).to include("type" => "LPAREN", "value" => "(") - expect(tokens[5]).to include("type" => "RPAREN", "value" => ")") - expect(tokens[6]).to include("type" => "PROCESSING_INSTRUCTION", "value" => "processing-instruction") - expect(tokens[7]).to include("type" => "LPAREN", "value" => "(") - expect(tokens[8]).to include("type" => "RPAREN", "value" => ")") - expect(tokens[9]).to include("type" => "NODE", "value" => "node") - expect(tokens[10]).to include("type" => "LPAREN", "value" => "(") - expect(tokens[11]).to include("type" => "RPAREN", "value" => ")") - end - end - - describe "complex expressions" do - it "tokenizes function calls" do - tokens = Taurus::XPath.tokenize("count(/root/child)") - - expect(tokens.size).to eq(7) - expect(tokens[0]).to include("type" => "NCNAME", "value" => "count") - expect(tokens[1]).to include("type" => "LPAREN", "value" => "(") - expect(tokens[2]).to include("type" => "SLASH", "value" => "/") - expect(tokens[3]).to include("type" => "NCNAME", "value" => "root") - expect(tokens[4]).to include("type" => "SLASH", "value" => "/") - expect(tokens[5]).to include("type" => "NCNAME", "value" => "child") - expect(tokens[6]).to include("type" => "RPAREN", "value" => ")") - end - - it "tokenizes predicates" do - tokens = Taurus::XPath.tokenize("/root/child[1]/@attr") - - expect(tokens.size).to eq(10) - expect(tokens[0]).to include("type" => "SLASH", "value" => "/") - expect(tokens[1]).to include("type" => "NCNAME", "value" => "root") - expect(tokens[2]).to include("type" => "SLASH", "value" => "/") - expect(tokens[3]).to include("type" => "NCNAME", "value" => "child") - expect(tokens[4]).to include("type" => "LBRACKET", "value" => "[") - expect(tokens[5]).to include("type" => "NUMBER", "value" => "1") - expect(tokens[6]).to include("type" => "RBRACKET", "value" => "]") - expect(tokens[7]).to include("type" => "SLASH", "value" => "/") - expect(tokens[8]).to include("type" => "AT", "value" => "@") - expect(tokens[9]).to include("type" => "NCNAME", "value" => "attr") - end - - it "tokenizes complex XPath expressions" do - xpath = "/library/book[@price < 20 and contains(title, 'Programming')]/author" - tokens = Taurus::XPath.tokenize(xpath) - - # Should have reasonable number of tokens - expect(tokens.size).to be > 10 - - # Should include various token types - token_types = tokens.map { |t| t["type"] } - expect(token_types).to include("SLASH") - expect(token_types).to include("NCNAME") - expect(token_types).to include("AT") - expect(token_types).to include("LT") - expect(token_types).to include("AND") - expect(token_types).to include("LBRACKET") - expect(token_types).to include("RBRACKET") - end - end - - describe "error handling" do - it "handles invalid characters" do - expect { Taurus::XPath.tokenize("invalid$char") }.to raise_error(RuntimeError) - end - - it "handles unterminated strings" do - expect { Taurus::XPath.tokenize("'unterminated") }.to raise_error(RuntimeError) - end - end - - describe "whitespace handling" do - it "ignores whitespace between tokens" do - tokens = Taurus::XPath.tokenize(" / root / child ") - - expect(tokens.size).to eq(4) - expect(tokens[0]).to include("type" => "SLASH", "value" => "/") - expect(tokens[1]).to include("type" => "NCNAME", "value" => "root") - expect(tokens[2]).to include("type" => "SLASH", "value" => "/") - expect(tokens[3]).to include("type" => "NCNAME", "value" => "child") - end - end - - describe "position information" do - it "provides line and column information" do - tokens = Taurus::XPath.tokenize("/root\n/child") - - expect(tokens.size).to eq(4) - expect(tokens[0]).to include("line" => 1, "column" => 1) - expect(tokens[1]).to include("line" => 1, "column" => 2) - expect(tokens[2]).to include("line" => 2, "column" => 1) - expect(tokens[3]).to include("line" => 2, "column" => 2) - end - end -end - -# Helper method for testing axis names -def xpath_token_is_axis_name?(type) - %w[ANCESTOR ANCESTOR_OR_SELF ATTRIBUTE CHILD DESCENDANT DESCENDANT_OR_SELF FOLLOWING FOLLOWING_SIBLING NAMESPACE PARENT PRECEDING PRECEDING_SIBLING SELF].include?(type) -end \ No newline at end of file diff --git a/spec/taurus/xpath/parser_spec.rb b/spec/taurus/xpath/parser_spec.rb deleted file mode 100644 index 490833f..0000000 --- a/spec/taurus/xpath/parser_spec.rb +++ /dev/null @@ -1,427 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe "Taurus::XPath.parse" do - describe "primary expressions" do - it "parses number literals" do - ast = Taurus::XPath.parse("42") - expect(ast["type"]).to eq("NUMBER") - expect(ast["number_value"]).to eq(42.0) - end - - it "parses decimal numbers" do - ast = Taurus::XPath.parse("3.14") - expect(ast["type"]).to eq("NUMBER") - expect(ast["number_value"]).to eq(3.14) - end - - it "parses string literals with single quotes" do - ast = Taurus::XPath.parse("'hello'") - expect(ast["type"]).to eq("STRING") - expect(ast["value"]).to eq("hello") - end - - it "parses string literals with double quotes" do - ast = Taurus::XPath.parse('"world"') - expect(ast["type"]).to eq("STRING") - expect(ast["value"]).to eq("world") - end - - it "parses parenthesized expressions" do - ast = Taurus::XPath.parse("(42)") - expect(ast["type"]).to eq("NUMBER") - expect(ast["number_value"]).to eq(42.0) - end - - it "parses function calls with no arguments" do - ast = Taurus::XPath.parse("node()") - expect(ast["type"]).to eq("FUNCTION_CALL") - expect(ast["value"]).to eq("node") - expect(ast["children"]).to be_nil.or(be_empty) - end - - it "parses function calls with one argument" do - ast = Taurus::XPath.parse("count(//item)") - expect(ast["type"]).to eq("FUNCTION_CALL") - expect(ast["value"]).to eq("count") - expect(ast["children"]).to be_an(Array) - expect(ast["children"].size).to eq(1) - end - - it "parses function calls with multiple arguments" do - ast = Taurus::XPath.parse("substring('hello', 1, 3)") - expect(ast["type"]).to eq("FUNCTION_CALL") - expect(ast["value"]).to eq("substring") - expect(ast["children"].size).to eq(3) - end - end - - describe "arithmetic operators" do - it "parses addition" do - ast = Taurus::XPath.parse("1 + 2") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("PLUS") - expect(ast["children"].size).to eq(2) - end - - it "parses subtraction" do - ast = Taurus::XPath.parse("5 - 3") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("MINUS") - end - - it "parses multiplication" do - ast = Taurus::XPath.parse("2 * 3") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("MULTIPLY") - end - - it "parses division" do - ast = Taurus::XPath.parse("10 div 2") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("DIV") - end - - it "parses modulo" do - ast = Taurus::XPath.parse("10 mod 3") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("MOD") - end - - it "parses unary negation" do - ast = Taurus::XPath.parse("-5") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("NEGATION") - expect(ast["children"].size).to eq(1) - end - end - - describe "comparison operators" do - it "parses equality" do - ast = Taurus::XPath.parse("@id = '123'") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("EQUAL") - end - - it "parses inequality" do - ast = Taurus::XPath.parse("@type != 'hidden'") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("NOT_EQUAL") - end - - it "parses less than" do - ast = Taurus::XPath.parse("price < 20") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("LESS") - end - - it "parses less than or equal" do - ast = Taurus::XPath.parse("quantity <= 10") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("LESS_EQUAL") - end - - it "parses greater than" do - ast = Taurus::XPath.parse("price > 100") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("GREATER") - end - - it "parses greater than or equal" do - ast = Taurus::XPath.parse("age >= 18") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("GREATER_EQUAL") - end - end - - describe "logical operators" do - it "parses and operator" do - ast = Taurus::XPath.parse("@enabled and @visible") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("AND") - end - - it "parses or operator" do - ast = Taurus::XPath.parse("@type = 'admin' or @type = 'moderator'") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("OR") - end - - it "parses complex boolean expression" do - ast = Taurus::XPath.parse("(@enabled and @visible) or @forced") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("OR") - end - end - - describe "union operator" do - it "parses simple union" do - ast = Taurus::XPath.parse("//book | //magazine") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("UNION") - end - - it "parses multiple unions" do - ast = Taurus::XPath.parse("//book | //magazine | //newspaper") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("UNION") - # Should be left-associative: (book | magazine) | newspaper - end - end - - describe "operator precedence" do - it "handles multiplication before addition" do - ast = Taurus::XPath.parse("1 + 2 * 3") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("PLUS") - # Right child should be multiplication - expect(ast["children"][1]["operator"]).to eq("MULTIPLY") - end - - it "handles comparison before and" do - ast = Taurus::XPath.parse("@x = 1 and @y = 2") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("AND") - # Both children should be EQUAL - expect(ast["children"][0]["operator"]).to eq("EQUAL") - expect(ast["children"][1]["operator"]).to eq("EQUAL") - end - - it "handles and before or" do - ast = Taurus::XPath.parse("@a and @b or @c") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("OR") - # Left child should be AND - expect(ast["children"][0]["operator"]).to eq("AND") - end - - it "respects parentheses" do - ast = Taurus::XPath.parse("(1 + 2) * 3") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("MULTIPLY") - # Left child should be addition - expect(ast["children"][0]["operator"]).to eq("PLUS") - end - end - - describe "simple path expressions" do - it "parses absolute path with single step" do - ast = Taurus::XPath.parse("/root") - expect(ast["type"]).to eq("ABSOLUTE_PATH") - expect(ast["children"]).to be_an(Array) - end - - it "parses absolute path with multiple steps" do - ast = Taurus::XPath.parse("/root/child/grandchild") - expect(ast["type"]).to eq("ABSOLUTE_PATH") - rel_path = ast["children"][0] - expect(rel_path["type"]).to eq("RELATIVE_PATH") - expect(rel_path["children"].size).to eq(3) - end - - it "parses descendant-or-self shorthand" do - ast = Taurus::XPath.parse("//book") - expect(ast["type"]).to eq("ABSOLUTE_PATH") - # Should have descendant-or-self step - expect(ast["children"].size).to eq(2) - end - - it "parses relative path" do - ast = Taurus::XPath.parse("child/grandchild") - expect(ast["type"]).to eq("RELATIVE_PATH") - expect(ast["children"].size).to eq(2) - end - - it "parses self abbreviation" do - ast = Taurus::XPath.parse(".") - expect(ast["type"]).to eq("STEP") - expect(ast["value"]).to eq("self") - end - - it "parses parent abbreviation" do - ast = Taurus::XPath.parse("..") - expect(ast["type"]).to eq("STEP") - expect(ast["value"]).to eq("parent") - end - - it "parses attribute with @ abbreviation" do - ast = Taurus::XPath.parse("@id") - expect(ast["type"]).to eq("STEP") - expect(ast["value"]).to eq("attribute") - end - end - - describe "axis specifiers" do - it "parses child axis" do - ast = Taurus::XPath.parse("child::book") - expect(ast["type"]).to eq("STEP") - expect(ast["value"]).to eq("child") - end - - it "parses descendant axis" do - ast = Taurus::XPath.parse("descendant::item") - expect(ast["type"]).to eq("STEP") - expect(ast["value"]).to eq("descendant") - end - - it "parses parent axis" do - ast = Taurus::XPath.parse("parent::book") - expect(ast["type"]).to eq("STEP") - expect(ast["value"]).to eq("parent") - end - - it "parses ancestor axis" do - ast = Taurus::XPath.parse("ancestor::section") - expect(ast["type"]).to eq("STEP") - expect(ast["value"]).to eq("ancestor") - end - - it "parses following-sibling axis" do - ast = Taurus::XPath.parse("following-sibling::chapter") - expect(ast["type"]).to eq("STEP") - expect(ast["value"]).to eq("following-sibling") - end - - it "parses attribute axis" do - ast = Taurus::XPath.parse("attribute::id") - expect(ast["type"]).to eq("STEP") - expect(ast["value"]).to eq("attribute") - end - end - - describe "node tests" do - it "parses wildcard" do - ast = Taurus::XPath.parse("//*") - rel_path = ast["children"][1] - step = rel_path["children"][0] - node_test = step["children"][0] - expect(node_test["type"]).to eq("NODE_TEST_ALL") - end - - it "parses name test" do - ast = Taurus::XPath.parse("//book") - # Navigate to node test - rel_path = ast["children"][1] - step = rel_path["children"][0] - node_test = step["children"][0] - expect(node_test["type"]).to eq("NODE_TEST_NAME") - expect(node_test["value"]).to eq("book") - end - - it "parses text() node test" do - ast = Taurus::XPath.parse("//text()") - rel_path = ast["children"][1] - step = rel_path["children"][0] - node_test = step["children"][0] - expect(node_test["type"]).to eq("NODE_TEST_TYPE") - expect(node_test["value"]).to eq("text") - end - - it "parses comment() node test" do - ast = Taurus::XPath.parse("//comment()") - rel_path = ast["children"][1] - step = rel_path["children"][0] - node_test = step["children"][0] - expect(node_test["type"]).to eq("NODE_TEST_TYPE") - expect(node_test["value"]).to eq("comment") - end - - it "parses node() node test" do - ast = Taurus::XPath.parse("//node()") - rel_path = ast["children"][1] - step = rel_path["children"][0] - node_test = step["children"][0] - expect(node_test["type"]).to eq("NODE_TEST_TYPE") - expect(node_test["value"]).to eq("node") - end - end - - describe "predicates" do - it "parses simple predicate with position" do - ast = Taurus::XPath.parse("//book[1]") - # Navigate through structure - rel_path = ast["children"][1] - step = rel_path["children"][0] - # Step should have node test and predicate as children - expect(step["children"].size).to eq(2) - predicate = step["children"][1] - expect(predicate["type"]).to eq("NUMBER") - end - - it "parses predicate with attribute test" do - ast = Taurus::XPath.parse("//book[@id='123']") - rel_path = ast["children"][1] - step = rel_path["children"][0] - expect(step["children"].size).to eq(2) - predicate = step["children"][1] - expect(predicate["type"]).to eq("OPERATOR") - expect(predicate["operator"]).to eq("EQUAL") - end - - it "parses multiple predicates" do - ast = Taurus::XPath.parse("//book[@lang='en'][1]") - rel_path = ast["children"][1] - step = rel_path["children"][0] - # Should have node test + 2 predicates - expect(step["children"].size).to eq(3) - end - - it "parses nested predicates" do - ast = Taurus::XPath.parse("//section[chapter[@published='true']]") - rel_path = ast["children"][1] - step = rel_path["children"][0] - expect(step["children"].size).to eq(2) - end - end - - describe "complex expressions" do - it "parses path with multiple steps and predicates" do - ast = Taurus::XPath.parse("/library/books/book[@category='fiction'][1]/title") - expect(ast["type"]).to eq("ABSOLUTE_PATH") - end - - it "parses expression with function calls" do - ast = Taurus::XPath.parse("count(//book[price > 20])") - expect(ast["type"]).to eq("FUNCTION_CALL") - expect(ast["value"]).to eq("count") - end - - it "parses complex predicate with operators" do - ast = Taurus::XPath.parse("//book[price > 10 and price < 50]") - rel_path = ast["children"][1] - step = rel_path["children"][0] - predicate = step["children"][1] - expect(predicate["operator"]).to eq("AND") - end - - it "parses union with predicates" do - ast = Taurus::XPath.parse("//book[@type='new'] | //magazine[@type='new']") - expect(ast["type"]).to eq("OPERATOR") - expect(ast["operator"]).to eq("UNION") - end - - it "parses path continuing after filter expression" do - ast = Taurus::XPath.parse("(//book)[1]/title") - expect(ast["type"]).to eq("PATH_EXPR") - end - end - - describe "error handling" do - it "raises error for unterminated string" do - expect { Taurus::XPath.parse("'unterminated") }.to raise_error(RuntimeError, /Unterminated string/) - end - - it "raises error for unexpected token" do - expect { Taurus::XPath.parse("@") }.to raise_error(RuntimeError, /parsing error/) - end - - it "raises error for unmatched parenthesis" do - expect { Taurus::XPath.parse("(1 + 2") }.to raise_error(RuntimeError, /Expected '\)'/) - end - - it "raises error for unmatched bracket" do - expect { Taurus::XPath.parse("//book[1") }.to raise_error(RuntimeError, /Expected '\]'/) - end - end -end \ No newline at end of file diff --git a/spec/taurus/xpath_comparison_predicates_spec.rb b/spec/taurus/xpath_comparison_predicates_spec.rb deleted file mode 100644 index 8ffb672..0000000 --- a/spec/taurus/xpath_comparison_predicates_spec.rb +++ /dev/null @@ -1,196 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe "XPath Comparison Predicates" do - let(:xml) do - <<~XML - - - - Ruby Basics - - - Advanced Ruby - - - Learning Ruby - - - Ruby Mastery - - - XML - end - - let(:doc) { Taurus.parse(xml) } - - describe "Greater than (>)" do - it "filters by price > 20" do - results = doc.xpath("//book[@price > 20]") - expect(results.size).to eq(2) - expect(results.map { |b| b[:id] }).to contain_exactly("2", "4") - end - - it "filters by stock > 3" do - results = doc.xpath("//book[@stock > 3]") - expect(results.size).to eq(2) - expect(results.map { |b| b[:id] }).to contain_exactly("1", "3") - end - - it "handles decimal comparisons" do - results = doc.xpath("//book[@price > 19.5]") - expect(results.size).to eq(3) - expect(results.map { |b| b[:id] }).to contain_exactly("1", "2", "4") - end - end - - describe "Greater than or equal (>=)" do - it "filters by price >= 29.99" do - results = doc.xpath("//book[@price >= 29.99]") - expect(results.size).to eq(2) - expect(results.map { |b| b[:id] }).to contain_exactly("2", "4") - end - - it "filters by stock >= 5" do - results = doc.xpath("//book[@stock >= 5]") - expect(results.size).to eq(2) - expect(results.map { |b| b[:id] }).to contain_exactly("1", "3") - end - - it "includes exact matches" do - results = doc.xpath("//book[@price >= 19.99]") - expect(results.size).to eq(3) - end - end - - describe "Less than (<)" do - it "filters by price < 20" do - results = doc.xpath("//book[@price < 20]") - expect(results.size).to eq(2) - expect(results.map { |b| b[:id] }).to contain_exactly("1", "3") - end - - it "filters by stock < 5" do - results = doc.xpath("//book[@stock < 5]") - expect(results.size).to eq(2) - expect(results.map { |b| b[:id] }).to contain_exactly("2", "4") - end - - it "handles decimal comparisons" do - results = doc.xpath("//book[@price < 16]") - expect(results.size).to eq(1) - expect(results.first[:id]).to eq("3") - end - end - - describe "Less than or equal (<=)" do - it "filters by price <= 19.99" do - results = doc.xpath("//book[@price <= 19.99]") - expect(results.size).to eq(2) - expect(results.map { |b| b[:id] }).to contain_exactly("1", "3") - end - - it "filters by stock <= 3" do - results = doc.xpath("//book[@stock <= 3]") - expect(results.size).to eq(2) - expect(results.map { |b| b[:id] }).to contain_exactly("2", "4") - end - - it "includes exact matches" do - results = doc.xpath("//book[@price <= 15.50]") - expect(results.size).to eq(1) - end - end - - describe "Equality (=)" do - it "filters by exact price match" do - results = doc.xpath("//book[@price = 29.99]") - expect(results.size).to eq(2) - expect(results.map { |b| b[:id] }).to contain_exactly("2", "4") - end - - it "filters by exact stock match" do - results = doc.xpath("//book[@stock = 0]") - expect(results.size).to eq(1) - expect(results.first[:id]).to eq("4") - end - - it "handles string equality" do - results = doc.xpath("//book[@id = '1']") - expect(results.size).to eq(1) - expect(results.first[:id]).to eq("1") - end - end - - describe "Inequality (!=)" do - it "filters by price != 29.99" do - results = doc.xpath("//book[@price != 29.99]") - expect(results.size).to eq(2) - expect(results.map { |b| b[:id] }).to contain_exactly("1", "3") - end - - it "filters by stock != 0" do - results = doc.xpath("//book[@stock != 0]") - expect(results.size).to eq(3) - expect(results.map { |b| b[:id] }).to contain_exactly("1", "2", "3") - end - end - - describe "Combined predicates" do - it "handles multiple comparison predicates" do - results = doc.xpath("//book[@price > 15 and @price < 25]") - expect(results.size).to eq(2) - expect(results.map { |b| b[:id] }).to contain_exactly("1", "3") - end - - it "handles comparison with position predicate" do - results = doc.xpath("//book[@price > 20][1]") - expect(results.size).to eq(1) - expect(results.first[:id]).to eq("2") - end - - it "handles OR conditions" do - results = doc.xpath("//book[@price < 16 or @price > 28]") - expect(results.size).to eq(3) - expect(results.map { |b| b[:id] }).to contain_exactly("2", "3", "4") - end - end - - describe "Edge cases" do - it "handles zero comparisons" do - results = doc.xpath("//book[@stock = 0]") - expect(results.size).to eq(1) - expect(results.first[:id]).to eq("4") - end - - it "handles empty results" do - results = doc.xpath("//book[@price > 100]") - expect(results).to be_empty - end - - it "handles comparison with non-numeric values gracefully" do - # If attribute doesn't exist or isn't numeric, should convert to NaN - # and comparisons with NaN are always false - results = doc.xpath("//book[@nonexistent > 5]") - expect(results).to be_empty - end - end - - describe "Type coercion" do - it "converts string attributes to numbers for comparison" do - results = doc.xpath("//book[@price > 20]") - expect(results.size).to eq(2) - end - - it "handles integer comparisons" do - results = doc.xpath("//book[@stock > 3]") - expect(results.size).to eq(2) - end - - it "handles float comparisons" do - results = doc.xpath("//book[@price > 19.5]") - expect(results.size).to eq(3) - end - end -end \ No newline at end of file diff --git a/spec/taurus/xpath_errors_spec.rb b/spec/taurus/xpath_errors_spec.rb deleted file mode 100644 index a6ff8fa..0000000 --- a/spec/taurus/xpath_errors_spec.rb +++ /dev/null @@ -1,280 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'XPath Error Messages' do - let(:xml) { 'textmore' } - let(:doc) { Taurus.parse(xml) } - - describe 'Syntax Errors' do - it 'reports incomplete path with position' do - expect { - doc.xpath('//') # Incomplete path - missing node test - }.to raise_error(Taurus::XPathError) do |error| - expect(error.message).to include('Expected') - expect(error.code).to eq(:xpath_syntax) - expect(error.line).to be > 0 - expect(error.column).to be > 0 - expect(error.context).not_to be_nil - end - end - - it 'reports unclosed predicate' do - expect { - doc.xpath('//item[') # Unclosed predicate - }.to raise_error(Taurus::XPathError) do |error| - expect(error.code).to eq(:xpath_syntax) - expect(error.message).to include("Unexpected token") - expect(error.line).to eq(1) - end - end - - it 'reports unclosed parenthesis' do - expect { - doc.xpath('//item[position(') # Unclosed function call - }.to raise_error(Taurus::XPathError) do |error| - expect(error.code).to eq(:xpath_syntax) - expect(error.message).to include("Unexpected token") - end - end - - it 'reports unexpected token after complete expression' do - expect { - doc.xpath('//item ]') # Extra closing bracket - }.to raise_error(Taurus::XPathError) do |error| - expect(error.code).to eq(:xpath_syntax) - expect(error.message).to include('Unexpected token') - expect(error.column).to be > 0 - end - end - - it 'reports incomplete expression in predicate' do - expect { - doc.xpath('//item[@id = 1 and ]') # Incomplete and expression - }.to raise_error(Taurus::XPathError) do |error| - expect(error.code).to eq(:xpath_syntax) - expect(error.line).to eq(1) - end - end - - it 'reports unterminated string literal' do - expect { - doc.xpath("//item[@id = 'test") # Missing closing quote - }.to raise_error(Taurus::XPathError) do |error| - expect(error.code).to eq(:xpath_syntax) - expect(error.message).to include('Unexpected token') - end - end - - it 'reports invalid character in expression' do - expect { - doc.xpath('//item[@id = #123]') # Invalid # character - }.to raise_error(Taurus::XPathError) do |error| - expect(error.code).to eq(:xpath_syntax) - expect(error.message).to include('Unexpected token') - end - end - - it 'reports unexpected colon outside QName' do - expect { - doc.xpath('//item/:child') # Invalid bare colon - }.to raise_error(Taurus::XPathError) do |error| - expect(error.code).to eq(:xpath_syntax) - expect(error.message).to include('node test') - end - end - - it 'reports unexpected exclamation mark' do - expect { - doc.xpath('//item[!]') # Invalid ! without = - }.to raise_error(Taurus::XPathError) do |error| - expect(error.code).to eq(:xpath_syntax) - expect(error.message).to include('Unexpected token') - end - end - end - - describe 'Unknown Function Errors' do - it 'reports unknown function name' do - expect { - doc.xpath('unknown-function()') - }.to raise_error(Taurus::XPathError) do |error| - expect(error.message).to include('unknown-function') - expect(error.code).to eq(:xpath_function) - end - end - - it 'reports unknown function with position' do - expect { - doc.xpath('not_a_real_xpath_function_xyz()') - }.to raise_error(Taurus::XPathError) do |error| - expect(error.code).to eq(:xpath_function) - expect(error.line).to eq(1) - expect(error.column).to be > 0 - end - end - end - - describe 'Error Context Snippets' do - it 'shows context for syntax errors at start' do - expect { - doc.xpath('[invalid') - }.to raise_error(Taurus::XPathError) do |error| - expect(error.context).not_to be_nil - expect(error.context).to include('[invalid') - end - end - - it 'shows context for errors in middle of expression' do - expect { - doc.xpath('//item[@id = 1 and @name = ]') - }.to raise_error(Taurus::XPathError) do |error| - expect(error.context).not_to be_nil - # Should show context around the error position - expect(error.context.length).to be > 0 - end - end - - it 'shows context for errors at end of expression' do - expect { - doc.xpath('//item[@id =') - }.to raise_error(Taurus::XPathError) do |error| - expect(error.context).not_to be_nil - expect(error.context).to include('@id') - end - end - - it 'includes error position marker in context' do - expect { - doc.xpath('//item[') - }.to raise_error(Taurus::XPathError) do |error| - # Context should include a position marker (e.g., ^) - expect(error.context).to match(/\^|\|/) - end - end - end - - describe 'Position Tracking Accuracy' do - it 'reports correct line for single-line expressions' do - expect { - doc.xpath('//item[@invalid') - }.to raise_error(Taurus::XPathError) do |error| - expect(error.line).to eq(1) - end - end - - it 'reports correct column for early errors' do - expect { - doc.xpath('[') - }.to raise_error(Taurus::XPathError) do |error| - expect(error.column).to eq(1) - end - end - - it 'reports correct column for mid-expression errors' do - expect { - doc.xpath('//item[@id = &]') - }.to raise_error(Taurus::XPathError) do |error| - expect(error.column).to be_between(14, 16) - end - end - - it 'reports correct position for EOF errors' do - expect { - doc.xpath('//item[position()') # Missing closing ] - }.to raise_error(Taurus::XPathError) do |error| - expect(error.line).to eq(1) - expect(error.column).to be > 15 - end - end - end - - describe 'Multiple Error Scenarios' do - it 'reports first error when multiple issues exist' do - expect { - doc.xpath('//item[[@id]') # Double [[ - first error should be reported - }.to raise_error(Taurus::XPathError) do |error| - expect(error.code).to eq(:xpath_syntax) - # Should report the first [ issue - end - end - - it 'provides helpful message for common mistakes' do - expect { - doc.xpath('//item[!@id]') # Should be 'not(@id)' - }.to raise_error(Taurus::XPathError) do |error| - expect(error.message).to include('Unexpected token') - end - end - end - - describe 'Error Message Quality' do - it 'includes expression text in error for short expressions' do - expect { - doc.xpath('[') - }.to raise_error(Taurus::XPathError) do |error| - # Context should include the invalid expression - expect(error.context).to include('[') - end - end - - it 'provides clear description of what was expected' do - expect { - doc.xpath('//item[') - }.to raise_error(Taurus::XPathError) do |error| - expect(error.message).to match(/expected|missing/i) - end - end - - it 'identifies token type that caused error' do - expect { - doc.xpath('//item ]') - }.to raise_error(Taurus::XPathError) do |error| - expect(error.message).to include('RBRACKET') - end - end - end - - describe 'Regression Tests' do - it 'handles valid expressions without errors' do - expect { doc.xpath('//item') }.not_to raise_error - expect { doc.xpath('//item[@id]') }.not_to raise_error - expect { doc.xpath('//item[1]') }.not_to raise_error - expect { doc.xpath('//item[@id="1"]') }.not_to raise_error - end - - it 'maintains existing XPath functionality' do - result = doc.xpath('//item[@id="1"]') - expect(result).to be_an(Array) - expect(result.length).to eq(1) - end - end - - describe 'Edge Cases' do - it 'handles empty expression' do - expect { - doc.xpath('') - }.to raise_error(Taurus::ParseError) do |error| - expect(error.code).to eq(:empty_input) - end - end - - it 'handles whitespace-only expression' do - expect { - doc.xpath(' ') - }.to raise_error(Taurus::XPathError) do |error| - expect(error.code).to eq(:xpath_syntax) - end - end - - it 'handles very long error positions correctly' do - long_path = '//item' + ('[@id]' * 50) + '[' - expect { - doc.xpath(long_path) - }.to raise_error(Taurus::XPathError) do |error| - expect(error.line).to eq(1) - expect(error.column).to be > 100 - end - end - end -end \ No newline at end of file diff --git a/spec/taurus/xpath_functions_spec.rb b/spec/taurus/xpath_functions_spec.rb deleted file mode 100644 index ce060b5..0000000 --- a/spec/taurus/xpath_functions_spec.rb +++ /dev/null @@ -1,180 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe "XPath Functions" do - describe "last() function" do - it "returns the last element in a nodeset" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('*[last()]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('c') - end - - it "works with position comparison" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('*[position() = last()]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('c') - end - - it "works in complex paths" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('section/item[last()]') - expect(result.size).to eq(1) - end - - it "works with descendant axis" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('descendant::*[last()]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('c') - end - - it "works with different node counts" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('*[last()]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('item') - end - - it "works with empty nodeset" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('*[last()]') - expect(result).to be_empty - end - - it "can be used in arithmetic expressions" do - xml = '
' - doc = parse(xml) - # last() - 1 should select the 3rd element (c) - result = doc.root.xpath('*[position() = last() - 1]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('c') - end - - it "works with multiple predicates" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('item[@id][last()]') - expect(result.size).to eq(1) - expect(result.first[:id]).to eq('3') - end - end - - describe "position() function" do - it "returns the position of each node" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('*[position() = 1]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('a') - end - - it "works with position 2" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('*[position() = 2]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('b') - end - - it "works with position 3" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('*[position() = 3]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('c') - end - - it "can be compared with last()" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('*[position() = last()]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('c') - end - - it "works with greater than operator" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('*[position() > 2]') - expect(result.size).to eq(2) - expect(result.map(&:name)).to eq(['c', 'd']) - end - - it "works with less than operator" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('*[position() < 3]') - expect(result.size).to eq(2) - expect(result.map(&:name)).to eq(['a', 'b']) - end - - it "works with arithmetic" do - xml = '
' - doc = parse(xml) - # position() + 1 = 3 means position 2 - result = doc.root.xpath('*[position() + 1 = 3]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('b') - end - - it "works in complex paths" do - xml = '
' - doc = parse(xml) - result = doc.root.xpath('section/item[position() = 2]') - expect(result.size).to eq(1) - end - - it "works with descendant axis" do - xml = '
' - doc = parse(xml) - # descendant::* gives [section, a, b, c], position 2 is 'a' - result = doc.root.xpath('descendant::*[position() = 2]') - expect(result.size).to eq(1) - expect(result.first.name).to eq('a') - end - - it "returns empty for out of range position" do - xml = '' - doc = parse(xml) - result = doc.root.xpath('*[position() = 10]') - expect(result).to be_empty - end - end - - describe "function call error handling" do - it "handles unknown function gracefully" do - xml = '' - doc = parse(xml) - expect { - doc.root.xpath('*[unknown-function()]') - }.to raise_error(RuntimeError, /Unknown function/) - end - - it "validates argument count for last()" do - xml = '' - doc = parse(xml) - # last() takes no arguments, this should fail at evaluation - # Note: This might be caught by parser first - expect { - doc.root.xpath('*[last(1)]') - }.to raise_error(RuntimeError) - end - - it "validates argument count for position()" do - xml = '' - doc = parse(xml) - expect { - doc.root.xpath('*[position(1)]') - }.to raise_error(RuntimeError) - end - end -end \ No newline at end of file diff --git a/spec/taurus_spec.rb b/spec/taurus_spec.rb deleted file mode 100644 index 146e29a..0000000 --- a/spec/taurus_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require "taurus/document" - -RSpec.describe Taurus do - it "has a version number" do - expect(Taurus::VERSION).not_to be nil - end - - it "can parse XML" do - xml = "test" - doc = Taurus.parse(xml) - - expect(doc).to be_a(Taurus::Document) - expect(doc.root.name).to eq("root") - expect(doc.root.text).to eq("test") - end -end diff --git a/spec/xml/c14n_spec.rb b/spec/xml/c14n_spec.rb new file mode 100644 index 0000000..be712ad --- /dev/null +++ b/spec/xml/c14n_spec.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +require "taurus/xml" + +# C14N (Canonical XML) coverage. Exercises the v0.5.2 taurus_c14n_canonicalize_ex +# and taurus_c14n_canonicalize_subtree_ex entry points via the Ruby API. +# +# Each spec asserts the *shape* of canonical output (XML canonical form rules: +# UTF-8, normalized line endings, lexicographic attribute ordering, namespace +# declaration ordering, empty element expansion, entity/char ref expansion, +# double-quoted attribute values). Exact byte-equality against Nokogiri +# would be ideal but pulls in a heavy dependency; these specs check the +# canonicalization rules from the W3C C14N 1.0 spec. + +RSpec.describe "C14N via libtaurus" do + let(:simple_xml) { "text" } + + describe "Document#canonicalize — default (canonical 1.0, no comments)" do + it "returns canonical XML as a String" do + out = Taurus::XML::Document.parse(simple_xml).canonicalize + expect(out).to be_a(String) + expect(out).to include("") + expect(out).to include("" do + out = Taurus::XML::Document.parse("").canonicalize + expect(out).to include("") + expect(out).not_to include("") + end + + it "uses double quotes for attribute values" do + out = Taurus::XML::Document.parse(%q{}).canonicalize + expect(out).to include('a="1"') + end + + it "sorts attributes lexicographically" do + out = Taurus::XML::Document.parse(%q{}).canonicalize + expect(out).to include('a="2" m="3" z="1"') + end + + it "expands required character references in text (< and &)" do + # Per XML 1.0 / C14N 1.0, only < and & MUST be escaped in text content. + # > need not be escaped (libtaurus leaves it literal). + out = Taurus::XML::Document.parse("<&>").canonicalize + expect(out).to include("<&") + end + + it "preserves comments when with_comments: true" do + out = Taurus::XML::Document.parse("") + .canonicalize(with_comments: true) + expect(out).to include("") + end + + it "strips comments by default" do + out = Taurus::XML::Document.parse("").canonicalize + expect(out).not_to include("") + end + + it "supports C14N 1.1 mode" do + out = Taurus::XML::Document.parse(simple_xml) + .canonicalize(Taurus::XML::FFI::C14N_1_1) + expect(out).to include("") + expect(out).to include("") + a = doc.root.first_element_child + sub = a.canonicalize + expect(sub).to include("") + expect(sub).to include("").or include("") + expect(sub).not_to include("") + expect(sub).not_to include("") + end + + it "supports with_comments: true on subtree" do + doc = Taurus::XML::Document.parse("") + a = doc.root + expect(a.canonicalize(with_comments: true)).to include("") + end + end + + describe "exclusive C14N (mode: :exclusive)" do + let(:nsdoc) do + Taurus::XML::Document.parse(<<~XML) + + text + + XML + end + + it "drops namespace declarations not visibly used (canonical keeps them all)" do + canonical = nsdoc.canonicalize(mode: Taurus::XML::FFI::C14N_MODE_CANONICAL) + exclusive = nsdoc.canonicalize(mode: Taurus::XML::FFI::C14N_MODE_EXCLUSIVE) + # Both should produce valid output; exact difference is implementation- + # specific. Just verify both run and produce strings including the root. + expect(canonical).to be_a(String) + expect(exclusive).to be_a(String) + expect(canonical.length).to be > 0 + expect(exclusive.length).to be > 0 + end + + it "accepts the `exclusive: true` shortcut" do + explicit = nsdoc.canonicalize(mode: Taurus::XML::FFI::C14N_MODE_EXCLUSIVE) + shortcut = nsdoc.canonicalize(exclusive: true) + expect(shortcut).to eq(explicit) + end + + it "passes inclusive namespace prefixes" do + # Build a doc that uses a prefix visible only at root level + doc = Taurus::XML::Document.parse(<<~XML) + + + + XML + # Without inclusive namespaces, exclusive C14N keeps only visibly-used prefixes. + # With "ds" in inclusive list, libtaurus includes the declaration even if + # a consumer needs to know about it. Both should produce strings. + si = doc.at_xpath("//ds:SignedInfo", + { "ds" => "http://www.w3.org/2000/09/xmldsig#" }) + expect(si).not_to be_nil + without = si.canonicalize(Taurus::XML::FFI::C14N_1_0, nil, exclusive: true) + with = si.canonicalize(Taurus::XML::FFI::C14N_1_0, %w[ds], exclusive: true) + expect(without).to be_a(String) + expect(with).to be_a(String) + end + end + + describe "alias #c14n" do + it "matches #canonicalize" do + doc = Taurus::XML::Document.parse(simple_xml) + expect(doc.c14n).to eq(doc.canonicalize) + end + end + + describe "Nokogiri-compatible signature" do + it "accepts (mode, inclusive_namespaces, with_comments) positional args" do + # The first positional arg is the version (C14N_1_0 / C14N_1_1); + # the rest are keyword args matching Nokogiri-style usage. + doc = Taurus::XML::Document.parse(simple_xml) + out = doc.canonicalize(Taurus::XML::FFI::C14N_1_0, nil, with_comments: false) + expect(out).to include("") + end + end +end + +RSpec.describe "Element namespace mutation (v0.5.2 #181 fix)" do + it "adds a namespace declaration via #add_namespace_definition" do + doc = Taurus::XML::Document.parse("") + ns = doc.root.add_namespace_definition("foo", "http://foo.example") + expect(ns).to be_a(Taurus::XML::Namespace) + expect(ns.prefix).to eq("foo") + expect(ns.href).to eq("http://foo.example") + end + + it "adds a default namespace via #default_namespace=" do + doc = Taurus::XML::Document.parse("") + doc.root.default_namespace = "http://default.example" + decls = doc.root.namespace_definitions + expect(decls.map(&:href)).to include("http://default.example") + default_decl = decls.find { |n| n.prefix.nil? } + expect(default_decl&.href).to eq("http://default.example") + end + + it "removes a namespace declaration via #remove_namespace_definition" do + doc = Taurus::XML::Document.parse("") + doc.root.add_namespace_definition("foo", "http://foo.example") + doc.root.remove_namespace_definition("foo") + expect(doc.root.namespace_definitions.length).to eq(0) + end +end + +RSpec.describe "previous_sibling for non-element nodes (v0.5.2 #179 fix)" do + it "walks backward through text nodes via FFI (no workaround)" do + doc = Taurus::XML::Document.parse("middle") + b = doc.root.element_children[1] + text = b.previous_sibling + expect(text).to be_a(Taurus::XML::Text) + a = text.previous_sibling + expect(a).to be_a(Taurus::XML::Element) + expect(a.name).to eq("a") + end + + it "returns nil at the start of the sibling chain" do + doc = Taurus::XML::Document.parse("") + a = doc.root.first_element_child + expect(a.previous_sibling).to be_nil + end +end diff --git a/spec/xml/css_spec.rb b/spec/xml/css_spec.rb new file mode 100644 index 0000000..6e9d8e6 --- /dev/null +++ b/spec/xml/css_spec.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +require "taurus/xml" + +RSpec.describe "Taurus::XML CSS selectors (minimal Nokogiri subset)" do + let(:doc) do + Taurus::XML::Document.parse(<<~HTML) + + + + + + HTML + end + + describe "tag selectors" do + it "matches by tag name" do + expect(doc.css("h1").map(&:content)).to eq(["Title"]) + expect(doc.css("p").length).to eq(2) + end + + it "matches * as wildcard" do + all = doc.css("*") + # Every element in the tree: html, body, div, h1, p, p, ul, li, li, li, a + expect(all.length).to be >= 11 + end + end + + describe "class selectors" do + it "matches .class" do + items = doc.css(".item") + expect(items.length).to eq(3) + expect(items.map(&:content)).to eq(%w[A B C]) + end + + it "matches tag.class" do + leads = doc.css("p.lead") + expect(leads.length).to eq(1) + expect(leads.first.content).to eq("Intro") + end + end + + describe "id selectors" do + it "matches #id" do + main = doc.css("#main") + expect(main.length).to eq(1) + expect(main.first["id"]).to eq("main") + end + + it "matches tag#id" do + div = doc.css("div#main") + expect(div.length).to eq(1) + end + end + + describe "attribute selectors" do + it "matches [attr]" do + with_href = doc.css("[href]") + expect(with_href.length).to eq(1) + expect(with_href.first.name).to eq("a") + end + + it "matches [attr=val]" do + links = doc.css(%q{[href='http://example.com']}) + expect(links.length).to eq(1) + end + + it "matches tag[attr=val]" do + a = doc.css("a[href='http://example.com']") + expect(a.first.content).to eq("Link") + end + end + + describe "descendant / child combinators" do + it "matches descendant (whitespace)" do + lis = doc.css("ul li") + expect(lis.length).to eq(3) + end + + it "matches direct child (>)" do + direct = doc.css("ul > li") + expect(direct.length).to eq(3) + # body > p should match both

children of body + body_ps = doc.css("body > div > p") + expect(body_ps.length).to eq(2) + end + + it "matches nested chains" do + special = doc.css("div#main ul li.special") + expect(special.length).to eq(1) + expect(special.first.content).to eq("B") + end + end + + describe "comma-separated multi-selectors" do + it "matches h1, h2, h3" do + result = doc.css("h1, h2, h3") + expect(result.map(&:name)).to eq(%w[h1]) + end + + it "matches div.container, p.lead" do + result = doc.css("div.container, p.lead") + expect(result.length).to eq(2) + end + end + + describe "pseudo-classes" do + it "matches :first-child" do + first = doc.css("li:first-child") + expect(first.length).to eq(1) + expect(first.first.content).to eq("A") + end + + it "matches :last-child" do + last = doc.css("li:last-child") + expect(last.length).to eq(1) + expect(last.first.content).to eq("C") + end + + it "matches tag:first-child on tag" do + first_p = doc.css("p:first-child") + # First

child of body — but body's first child is div, so this + # should return empty (no

is the first child of its parent). + expect(first_p.length).to eq(0) + end + end + + describe "at_css (first match)" do + it "returns the first matching node" do + lead = doc.at_css("p.lead") + expect(lead.content).to eq("Intro") + end + + it "returns nil if no match" do + expect(doc.at_css("nomatch")).to be_nil + end + end + + describe "search (auto-detect CSS vs XPath)" do + it "uses CSS when input doesn't look like XPath" do + result = doc.search("li.item") + expect(result.length).to eq(3) + end + + it "uses XPath when input looks like XPath" do + result = doc.search("//li") + expect(result.length).to eq(3) + end + end + + describe "unsupported selectors" do + it "raises ArgumentError for :nth-child(n)" do + expect { doc.css("li:nth-child(2)") } + .to raise_error(ArgumentError, /unsupported pseudo-class/) + end + end +end diff --git a/spec/xml/document_spec.rb b/spec/xml/document_spec.rb new file mode 100644 index 0000000..6857264 --- /dev/null +++ b/spec/xml/document_spec.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +require "taurus/xml" + +RSpec.describe Taurus::XML::Document do + describe ".parse" do + it "parses a simple XML string into a Document" do + doc = described_class.parse("Hello") + expect(doc).to be_a(described_class) + expect(doc).to respond_to(:root) + end + + it "raises ParseError on malformed input" do + expect { described_class.parse("") }.to raise_error(Taurus::XML::ParseError) + end + + it "accepts an IO object (responds to :read)" do + require "stringio" + io = StringIO.new("text") + doc = described_class.parse(io) + expect(doc.root.name).to eq("root") + end + end + + describe "#root" do + it "returns the root element" do + doc = described_class.parse("") + root = doc.root + expect(root).to be_a(Taurus::XML::Element) + expect(root.name).to eq("library") + end + + it "returns nil for empty document" do + # libtaurus parses an empty/whitespace doc to no root + doc = described_class.parse("") + expect(doc.root.name).to eq("x") + end + end + + describe "#free" do + it "can be called explicitly without error" do + doc = described_class.parse("") + expect { doc.free }.not_to raise_error + end + + it "is idempotent" do + doc = described_class.parse("") + doc.free + expect { doc.free }.not_to raise_error + end + end +end + +RSpec.describe Taurus::XML::Element do + let(:doc) { Taurus::XML::Document.parse(<<~XML) } + + Ruby + XML + + XML + let(:root) { doc.root } + + it "exposes element name" do + expect(root.name).to eq("library") + end + + it "exposes element text content (concatenated descendants)" do + expect(root.content).to include("Ruby").and include("XML") + end + + it "exposes attribute lookup via #[name]" do + expect(root["version"]).to eq("2.0") + expect(root["nonexistent"]).to be_nil + end + + it "exposes attribute keys and values" do + expect(root.keys).to eq(["version"]) + expect(root.values).to eq(["2.0"]) + end + + it "exposes the attributes hash" do + expect(root.attributes).to be_a(Hash) + expect(root.attributes.keys).to eq(["version"]) + expect(root.attributes["version"]).to be_a(Taurus::XML::Attr) + expect(root.attributes["version"].value).to eq("2.0") + end + + it "iterates element children" do + children = root.element_children.to_a + expect(children.length).to eq(2) + expect(children.map(&:name)).to eq(%w[book book]) + end + + it "iterates all children (elements + text)" do + all = root.children.to_a + expect(all.length).to be >= 2 + expect(all.first).to be_a(Taurus::XML::Text) # whitespace before first + end + + it "exposes first_element_child and last_element_child" do + expect(root.first_element_child["id"]).to eq("1") + expect(root.last_element_child["id"]).to eq("2") + end + + it "exposes parent navigation" do + book = root.first_element_child + expect(book.parent).to eq(root) + end + + it "exposes sibling navigation" do + book1 = root.first_element_child + book2 = book1.next_element + expect(book2["id"]).to eq("2") + expect(book2.previous_element).to eq(book1) + end +end + +RSpec.describe Taurus::XML::Node do + describe "type predicates" do + it "recognizes element nodes" do + doc = Taurus::XML::Document.parse("") + expect(doc.root.element?).to be true + expect(doc.root.text?).to be false + end + + it "recognizes text nodes" do + doc = Taurus::XML::Document.parse("hello") + text = doc.root.child + expect(text).to be_a(Taurus::XML::Text) + expect(text.text?).to be true + expect(text.element?).to be false + expect(text.content).to eq("hello") + end + + it "recognizes comment nodes" do + doc = Taurus::XML::Document.parse("") + comment = doc.root.children.find { |n| n.comment? } + expect(comment).to be_a(Taurus::XML::Comment) + expect(comment.content).to eq(" hi ") + end + + it "recognizes CDATA nodes" do + doc = Taurus::XML::Document.parse("]]>") + cdata = doc.root.children.find { |n| n.cdata? } + expect(cdata).to be_a(Taurus::XML::CDATA) + expect(cdata.content).to eq("") + end + + it "recognizes processing instruction nodes" do + doc = Taurus::XML::Document.parse("") + pi = doc.root.children.find { |n| n.processing_instruction? } + expect(pi).to be_a(Taurus::XML::ProcessingInstruction) + expect(pi.name).to eq("xml-stylesheet") + end + end + + describe "navigation" do + it "walks siblings via next_sibling" do + doc = Taurus::XML::Document.parse("") + root = doc.root + a = root.first_element_child + b = a.next_sibling + c = b.next_sibling + expect(c.name).to eq("c") + expect(c.next_sibling).to be_nil + end + + it "walks all children via children.each" do + doc = Taurus::XML::Document.parse("") + root = doc.root + names = root.children.select(&:element?).map(&:name) + expect(names).to eq(%w[a b c]) + end + end +end diff --git a/spec/xml/exclusive_c14n_spec.rb b/spec/xml/exclusive_c14n_spec.rb new file mode 100644 index 0000000..0c4c478 --- /dev/null +++ b/spec/xml/exclusive_c14n_spec.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +require "taurus/xml" + +# Real W3C Exclusive XML Canonicalization 1.0 (https://www.w3.org/2001/10/xml-exc-c14n#) +# coverage. v0.5.3 shipped the real algorithm; v0.5.2 was a stub routed to +# canonical. These specs assert the algorithm's distinguishing behaviors: +# +# 1. Visibly-used namespace prefixes are emitted on the element that uses them. +# 2. Unused inherited namespace prefixes are dropped (vs canonical, which keeps +# them in scope). +# 3. Caller-supplied inclusive namespace prefixes are force-included. +# 4. Output differs from canonical mode for the same input on namespace-heavy XML. + +RSpec.describe "Exclusive C14N (v0.5.3 #183 real implementation)" do + # Classic W3C exc-c14n example: ancestor declares two prefixes, + # uses n1, uses n2. Exclusive should emit each declaration on the + # element that uses it, not duplicate either on the other. + let(:xml) do + <<~XML + + + + + + XML + end + let(:doc) { Taurus::XML::Document.parse(xml) } + let(:b) { doc.root.first_element_child } + + describe "visibly-used prefix emission" do + it "emits xmlns:n1 on because n1 is visibly used there" do + exclusive = b.canonicalize(exclusive: true) + expect(exclusive) + .to match(%r{}) + end + + it "emits xmlns:n2 on because n2 is visibly used there" do + exclusive = b.canonicalize(exclusive: true) + expect(exclusive) + .to match(%r{}) + end + + it "does NOT emit xmlns:n2 on (visibly unused there)" do + exclusive = b.canonicalize(exclusive: true) + # The open tag itself must not declare n2. + b_open = exclusive[/]*>/] + expect(b_open).not_to include("xmlns:n2") + end + + it "does NOT emit xmlns:n1 on (visibly unused there)" do + exclusive = b.canonicalize(exclusive: true) + c_open = exclusive[/]*>/] + expect(c_open).not_to include("xmlns:n1") + end + end + + describe "canonical vs exclusive output differs" do + it "produces different output for the same namespace-heavy input" do + canonical = b.canonicalize(mode: Taurus::XML::FFI::C14N_MODE_CANONICAL) + exclusive = b.canonicalize(mode: Taurus::XML::FFI::C14N_MODE_EXCLUSIVE) + expect(canonical).not_to eq(exclusive) + end + + it "matches via the exclusive: true shortcut" do + explicit = b.canonicalize(mode: Taurus::XML::FFI::C14N_MODE_EXCLUSIVE) + shortcut = b.canonicalize(exclusive: true) + expect(shortcut).to eq(explicit) + end + end + + describe "inclusive namespace prefixes (caller force-include list)" do + it "force-includes a prefix the subtree doesn't visibly use" do + # only visibly uses n1. Force-include n2 in the output of + # via the inclusive list — useful for enveloped-signature cases. + out = b.canonicalize(Taurus::XML::FFI::C14N_1_0, %w[n2], exclusive: true) + b_open = out[/]*>/] + expect(b_open).to include("xmlns:n1=") + expect(b_open).to include("xmlns:n2=") + end + + it "force-includes a prefix not declared anywhere in scope" do + # 'undeclared' isn't in the document at all. libtaurus should still + # produce output (the prefix just won't resolve, but it shouldn't crash). + out = b.canonicalize(Taurus::XML::FFI::C14N_1_0, %w[undeclared], exclusive: true) + expect(out).to be_a(String) + expect(out).to include(" (its attribute) AND we ask to force-include n1. + out = b.canonicalize(Taurus::XML::FFI::C14N_1_0, %w[n1], exclusive: true) + b_open = out[/]*>/] + expect(b_open.scan(/xmlns:n1=/).length).to eq(1), + "expected exactly one xmlns:n1= on , got: #{b_open}" + end + + it "does not duplicate xmlns even when multiple prefixes overlap" do + # n1 is visibly used on , n2 is visibly used on . Force-include both. + # Per W3C exc-c14n §2.4, inclusive prefixes render on the apex element + # of the canonicalized subtree, so both xmlns:n1 and xmlns:n2 land on . + # then does NOT re-declare xmlns:n2 because the ancestor in the + # output already has it. + out = b.canonicalize(Taurus::XML::FFI::C14N_1_0, %w[n1 n2], exclusive: true) + b_open = out[/]*>/] + c_open = out[/]*>/] + expect(b_open.scan(/xmlns:n1=/).length).to eq(1), + "apex should declare xmlns:n1 exactly once" + expect(b_open.scan(/xmlns:n2=/).length).to eq(1), + "apex should declare xmlns:n2 once (force-included via inclusive list)" + expect(c_open.scan(/xmlns:n2=/).length).to eq(0), + " must not re-declare xmlns:n2 (already rendered by output ancestor)" + expect(c_open.scan(/xmlns:n1=/).length).to eq(0), + " must not declare xmlns:n1 (visibly unused there)" + end + end + + describe "subtree vs document scope" do + it "Element#canonicalize operates on the element's subtree" do + exclusive_subtree = b.canonicalize(exclusive: true) + # Subtree output starts with , not + expect(exclusive_subtree).to match(/\A\s* element. Exclusive C14N is used to canonicalize the + # signed subtree. A prefix declared on the enveloping ancestor is only + # emitted on the element that visibly uses it — never duplicated on + # descendants that don't. + it "emits a visibly-used declaration on the using element only" do + xml = <<~XML + + + + IBM + + + + XML + doc = Taurus::XML::Document.parse(xml) + body = doc.at_xpath("//soap:Body", + { "soap" => "http://schemas.xmlsoap.org/soap/envelope/" }) + expect(body).not_to be_nil + + exclusive = body.canonicalize(exclusive: true) + # soap:Body visibly uses soap (its own element name) → emit xmlns:soap on Body + body_open = exclusive[/]*>/] + expect(body_open).to include("xmlns:soap=") + # m:GetPrice visibly uses m → emit xmlns:m on GetPrice + expect(exclusive).to include("]*>/] + expect(getprice_open).to include("xmlns:m=") + # m:Symbol doesn't re-emit xmlns:m (already declared by visible ancestor in output) + symbol_open = exclusive[/]*>/] + expect(symbol_open).not_to include("xmlns:m=") + end + end +end diff --git a/spec/xml/ffi_spec.rb b/spec/xml/ffi_spec.rb new file mode 100644 index 0000000..4347814 --- /dev/null +++ b/spec/xml/ffi_spec.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require "taurus/xml" + +RSpec.describe Taurus::XML::FFI do + describe "library loading" do + it "is attached to libtaurus shared library" do + expect(described_class).to be_a(Module) + end + + it "exposes taurus_version as an attached function" do + expect(described_class).to respond_to(:taurus_version) + end + + it "returns a version string from taurus_version" do + version = described_class.taurus_version + expect(version).to be_a(String) + expect(version).to match(/\A\d+\.\d+\.\d+/) + end + end + + describe "document lifecycle" do + it "attaches taurus_parse_string and taurus_document_free" do + expect(described_class).to respond_to(:taurus_parse_string) + expect(described_class).to respond_to(:taurus_document_free) + expect(described_class).to respond_to(:taurus_document_root) + end + + it "attaches taurus_document_serialize and taurus_element_serialize" do + expect(described_class).to respond_to(:taurus_document_serialize) + expect(described_class).to respond_to(:taurus_element_serialize) + end + + it "attaches taurus_c14n_canonicalize" do + expect(described_class).to respond_to(:taurus_c14n_canonicalize) + end + end + + describe "node traversal" do + it "attaches the taurus_node_* family" do + %i[ + taurus_node_get_type + taurus_node_first_child + taurus_node_last_child + taurus_node_next_sibling + taurus_node_previous_sibling + taurus_node_child_count + taurus_node_as_element + taurus_element_as_node + ].each do |fn| + expect(described_class).to respond_to(fn), "missing #{fn}" + end + end + end + + describe "element operations" do + it "attaches element query functions" do + %i[ + taurus_element_name + taurus_element_text + taurus_element_attribute + taurus_element_attribute_count + taurus_element_attribute_name_at + taurus_element_attribute_value_at + taurus_element_parent + taurus_element_first_child_any + taurus_element_last_child_any + taurus_element_next_sibling_any + taurus_element_previous_sibling_any + ].each do |fn| + expect(described_class).to respond_to(fn), "missing #{fn}" + end + end + + it "attaches element mutation functions" do + %i[ + taurus_element_create + taurus_element_set_name + taurus_element_set_attribute + taurus_element_remove_attribute + taurus_element_remove_child + taurus_element_remove_children + taurus_element_append_child + taurus_element_prepend_child + taurus_element_insert_before + taurus_element_insert_after + taurus_element_set_text + ].each do |fn| + expect(described_class).to respond_to(fn), "missing #{fn}" + end + end + end + + describe "typed-node content getters" do + it "attaches content getters for text/comment/cdata/pi" do + %i[ + taurus_text_node_get_content + taurus_comment_node_get_content + taurus_cdata_node_get_content + taurus_pi_node_get_target + taurus_pi_node_get_data + ].each do |fn| + expect(described_class).to respond_to(fn), "missing #{fn}" + end + end + end + + describe "XPath" do + it "attaches the full xpath surface" do + %i[ + taurus_xpath_eval + taurus_xpath_eval_with_vars + taurus_xpath_result_type + taurus_xpath_result_count + taurus_xpath_result_get + taurus_xpath_result_boolean + taurus_xpath_result_number + taurus_xpath_result_string + taurus_xpath_result_free + taurus_xpath_variable_set_new + taurus_xpath_variable_set_free + taurus_xpath_variable_set_boolean + taurus_xpath_variable_set_number + taurus_xpath_variable_set_string + ].each do |fn| + expect(described_class).to respond_to(fn), "missing #{fn}" + end + end + end + + describe "SAX" do + it "attaches the sax surface" do + %i[ + taurus_sax_parse + taurus_sax_parser_create + taurus_sax_parser_feed + taurus_sax_parser_free + taurus_sax_parser_set_streaming + ].each do |fn| + expect(described_class).to respond_to(fn), "missing #{fn}" + end + end + + it "exposes the SAXHandler struct class" do + expect(described_class::SAXHandler).to be < ::FFI::Struct + end + + it "exposes the SerializeOptions struct class" do + expect(described_class::SerializeOptions).to be < ::FFI::Struct + end + end + + describe "namespaces" do + it "attaches namespace accessors" do + %i[ + taurus_element_namespace + taurus_namespace_uri + taurus_namespace_prefix + taurus_element_namespace_for_prefix + taurus_element_namespace_count + ].each do |fn| + expect(described_class).to respond_to(fn), "missing #{fn}" + end + end + end + + describe "memory + status" do + it "attaches taurus_free_string and taurus_xinclude_process" do + expect(described_class).to respond_to(:taurus_free_string) + expect(described_class).to respond_to(:taurus_xinclude_process) + end + + it "exposes status code constants" do + expect(described_class::TAURUS_OK).to eq(0) + expect(described_class::TAURUS_ERROR_PARSE).to eq(-2) + expect(described_class::TAURUS_ERROR_XPATH).to eq(-3) + end + + it "exposes xpath result type constants" do + expect(described_class::XPATH_NODESET).to eq(0) + expect(described_class::XPATH_BOOLEAN).to eq(1) + expect(described_class::XPATH_NUMBER).to eq(2) + expect(described_class::XPATH_STRING).to eq(3) + end + + it "exposes libtaurus raw node type constants" do + expect(described_class::NODE_ELEMENT).to eq(0) + expect(described_class::NODE_TEXT).to eq(1) + expect(described_class::NODE_COMMENT).to eq(2) + expect(described_class::NODE_CDATA).to eq(3) + expect(described_class::NODE_PI).to eq(4) + expect(described_class::NODE_DOCTYPE).to eq(5) + expect(described_class::NODE_ATTRIBUTE).to eq(6) + end + + it "exposes C14N version constants" do + expect(described_class::C14N_1_0).to eq(0) + expect(described_class::C14N_1_1).to eq(1) + end + end +end diff --git a/spec/xml/mutation_extras_spec.rb b/spec/xml/mutation_extras_spec.rb new file mode 100644 index 0000000..872d864 --- /dev/null +++ b/spec/xml/mutation_extras_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "taurus/xml" + +RSpec.describe "Element mutation: replace / swap / wrap / children=" do + let(:doc) { Taurus::XML::Document.parse("") } + let(:root) { doc.root } + + describe "#replace" do + it "replaces self with a created element at the same position" do + b = root.element_children[1] + new_elem = doc.create_element("X") + result = b.replace(new_elem) + expect(result).to eq(new_elem) + expect(root.element_children.map(&:name)).to eq(%w[a X c]) + end + + it "raises when self has no parent" do + elem = doc.create_element("orphan") + expect { elem.replace(doc.create_element("X")) } + .to raise_error(Taurus::XML::Error, /no parent/) + end + end + + describe "#swap" do + it "replaces self and returns self for chaining" do + b = root.element_children[1] + returned = b.swap(doc.create_element("X")) + expect(returned).to eq(b) + expect(root.element_children.map(&:name)).to eq(%w[a X c]) + end + end + + describe "#children=" do + it "replaces all children with the new collection" do + root.children = [doc.create_element("p"), doc.create_element("q")] + expect(root.element_children.map(&:name)).to eq(%w[p q]) + end + + it "clears children when given an empty array" do + root.children = [] + expect(root.element_children).to be_empty + end + end + + describe "#wrap" do + it "wraps self in a new element parsed from markup" do + b = root.element_children[1] + b.wrap("

") + expect(root.element_children.map(&:name)).to eq(%w[a div c]) + wrapper = root.element_children[1] + expect(wrapper["class"]).to eq("wrapped") + expect(wrapper.element_children.map(&:name)).to eq(%w[b]) + end + + it "raises when self has no parent" do + orphan = doc.create_element("orphan") + expect { orphan.wrap("") }.to raise_error(Taurus::XML::Error, /no parent/) + end + end +end diff --git a/spec/xml/sax_spec.rb b/spec/xml/sax_spec.rb new file mode 100644 index 0000000..72955f5 --- /dev/null +++ b/spec/xml/sax_spec.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +require "taurus/xml" +require "stringio" + +RSpec.describe Taurus::XML::SAX::Parser do + let(:xml) do + <<~XML + + + Ruby + XML + + XML + end + + # Test handler that captures every event into an array for assertion. + class RecordingHandler < Taurus::XML::SAX::Document + attr_reader :events + + def initialize + @events = [] + end + + def start_document; @events << [:start_document]; end + def end_document; @events << [:end_document]; end + + def start_element(name, attrs = []) + @events << [:start_element, name, attrs] + end + def end_element(name) + @events << [:end_element, name] + end + def characters(string) + @events << [:characters, string] unless string.strip.empty? + end + def comment(string) + @events << [:comment, string] + end + def cdata_block(string) + @events << [:cdata, string] + end + def processing_instruction(name, content) + @events << [:pi, name, content] + end + def start_prefix_mapping(prefix, uri) + @events << [:start_prefix, prefix, uri] + end + def end_prefix_mapping(prefix) + @events << [:end_prefix, prefix] + end + def error(message, line = 0, column = 0) + @events << [:error, message, line, column] + end + end + + describe "#parse_memory" do + it "delivers start_element / end_element events with attrs as [name, value] pairs" do + h = RecordingHandler.new + described_class.new(h).parse_memory(%q{}) + expect(h.events).to include([:start_element, "book", [["id", "1"], ["lang", "en"]]]) + expect(h.events).to include([:end_element, "book"]) + end + + it "delivers characters events" do + h = RecordingHandler.new + described_class.new(h).parse_memory("hello world") + chars = h.events.select { |e| e[0] == :characters }.map { |e| e[1] } + expect(chars.join).to eq("hello world") + end + + it "delivers comment events" do + h = RecordingHandler.new + described_class.new(h).parse_memory("") + expect(h.events).to include([:comment, " a note "]) + end + + it "delivers cdata events" do + h = RecordingHandler.new + described_class.new(h).parse_memory("]]>") + expect(h.events).to include([:cdata, ""]) + end + + it "delivers processing_instruction events" do + h = RecordingHandler.new + described_class.new(h).parse_memory(%q{}) + pis = h.events.select { |e| e[0] == :pi } + expect(pis.length).to eq(1) + expect(pis.first[1]).to eq("xml-stylesheet") + expect(pis.first[2]).to eq('type="text/xsl"') + end + + it "delivers start_prefix_mapping events for namespace declarations" do + h = RecordingHandler.new + described_class.new(h).parse_memory(%q{}) + prefix_events = h.events.select { |e| e[0] == :start_prefix } + expect(prefix_events).to include([:start_prefix, "foo", "http://foo.example"]) + end + + it "delivers start_document and end_document" do + h = RecordingHandler.new + described_class.new(h).parse_memory("") + expect(h.events.first).to eq([:start_document]) + expect(h.events.last).to eq([:end_document]) + end + + it "walks the full event sequence in document order" do + h = RecordingHandler.new + described_class.new(h).parse_memory(%q{text}) + tags = h.events.select { |e| %i[start_element end_element].include?(e[0]) } + .map { |e| [e[0], e[1]] } + expect(tags).to eq([ + [:start_element, "lib"], + [:start_element, "book"], + [:end_element, "book"], + [:end_element, "lib"] + ]) + end + end + + describe "#parse_io (streaming via feed)" do + it "parses the same events as parse_memory" do + h1 = RecordingHandler.new + h2 = RecordingHandler.new + described_class.new(h1).parse_memory(xml) + described_class.new(h2).parse_io(StringIO.new(xml)) + + # Streaming may split characters events across chunk boundaries. + # Compare element/structure events only. + structure = ->(events) do + events.select { |e| %i[start_element end_element comment cdata pi start_prefix].include?(e[0]) } + end + expect(structure.call(h1.events)).to eq(structure.call(h2.events)) + end + + it "handles chunks larger than the buffer" do + big = "" + ("x" * 10_000) + "" + h = RecordingHandler.new + described_class.new(h).parse_io(StringIO.new(big)) + tags = h.events.select { |e| %i[start_element end_element].include?(e[0]) } + .map { |e| [e[0], e[1]] } + expect(tags).to eq([[:start_element, "r"], [:end_element, "r"]]) + end + end + + describe "#parse (auto-dispatch)" do + it "uses parse_memory for strings" do + h = RecordingHandler.new + described_class.new(h).parse("") + expect(h.events).to include([:start_element, "x", []]) + end + + it "uses parse_io for IO objects" do + h = RecordingHandler.new + described_class.new(h).parse(StringIO.new("")) + expect(h.events).to include([:start_element, "x", []]) + end + + it "raises ArgumentError for unsupported types" do + expect { described_class.new.parse(42) } + .to raise_error(ArgumentError, /String or IO/) + end + end + + describe "#parse_file" do + it "parses from a file path" do + require "tmpdir" + path = File.join(Dir.mktmpdir, "test.xml") + File.write(path, "") + h = RecordingHandler.new + described_class.new(h).parse_file(path) + expect(h.events).to include([:start_element, "x", []]) + expect(h.events).to include([:start_element, "y", []]) + end + end + + describe "default Document handler" do + it "does not raise when no handler is supplied" do + expect { described_class.new.parse("") }.not_to raise_error + end + end +end diff --git a/spec/xml/v050_features_spec.rb b/spec/xml/v050_features_spec.rb new file mode 100644 index 0000000..2388b00 --- /dev/null +++ b/spec/xml/v050_features_spec.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require "taurus/xml" + +RSpec.describe "v0.5.0+ element mutation" do + let(:doc) { Taurus::XML::Document.parse("") } + let(:root) { doc.root } + + it "renames an element via #name=" do + root.name = "renamed" + expect(root.name).to eq("renamed") + end + + it "sets and removes attributes via []= and remove_attribute" do + root["id"] = "top" + expect(root["id"]).to eq("top") + root.remove_attribute("id") + expect(root["id"]).to be_nil + end + + it "sets text content via #content=" do + root.content = "fresh text" + expect(root.content).to eq("fresh text") + end + + it "appends a created element via #add_child" do + new_elem = doc.create_element("c") + root.add_child(new_elem) + expect(root.element_children.map(&:name)).to eq(%w[a b c]) + end + + it "supports << as add_child alias" do + root << doc.create_element("c") + expect(root.element_children.map(&:name)).to eq(%w[a b c]) + end + + it "prepends a child via #prepend_child" do + root.prepend_child(doc.create_element("z")) + expect(root.element_children.first.name).to eq("z") + end +end + +RSpec.describe "v0.5.0+ typed node creators" do + let(:doc) { Taurus::XML::Document.parse("") } + let(:root) { doc.root } + + it "creates and attaches a text node" do + text = doc.create_text_node("hello") + root.add_child(text) + expect(root.content).to eq("hello") + end + + it "creates and attaches a comment" do + comment = doc.create_comment("a note") + root.add_child(comment) + expect(root.children.find { |n| n.comment? }.content).to eq("a note") + end + + it "creates and attaches a CDATA section" do + cdata = doc.create_cdata("data") + root.add_child(cdata) + expect(root.children.find { |n| n.cdata? }.content).to eq("data") + end + + it "creates and attaches a processing instruction" do + pi = doc.create_processing_instruction("xml-stylesheet", 'type="text/xsl"') + root.add_child(pi) + pi_node = root.children.find { |n| n.processing_instruction? } + expect(pi_node.name).to eq("xml-stylesheet") + expect(pi_node.content).to eq('type="text/xsl"') + end + + it "supports Text#content= for existing text nodes" do + text = doc.create_text_node("initial") + root.add_child(text) + text.content = "updated" + expect(root.content).to eq("updated") + end + + it "supports Comment#content= for existing comment nodes" do + c = doc.create_comment("v1") + root.add_child(c) + c.content = "v2" + expect(c.content).to eq("v2") + end + + it "supports PI#target= and #data=" do + pi = doc.create_processing_instruction("foo", "bar") + root.add_child(pi) + pi.target = "baz" + pi.data = "qux" + expect(pi.name).to eq("baz") + expect(pi.content).to eq("qux") + end +end + +RSpec.describe "v0.5.0+ parent + unlink for non-element nodes" do + it "exposes #parent on a Text node (worked around in v0.4.4)" do + doc = Taurus::XML::Document.parse("hello") + text = doc.root.child + expect(text).to be_a(Taurus::XML::Text) + expect(text.parent).to eq(doc.root) + end + + it "exposes #parent on a Comment node" do + doc = Taurus::XML::Document.parse("") + comment = doc.root.children.find(&:comment?) + expect(comment.parent).to eq(doc.root) + end + + it "unlinks a Text node via #remove" do + doc = Taurus::XML::Document.parse("hello") + text = doc.root.child + text.remove + expect(doc.root.content).to eq("") + end +end + +RSpec.describe "v0.5.0+ namespace definitions" do + it "enumerates namespaces declared on an element" do + doc = Taurus::XML::Document.parse(<<~XML) + + + + XML + decls = doc.root.namespace_definitions + expect(decls.length).to eq(2) + prefixes = decls.map(&:prefix) + hrefs = decls.map(&:href) + expect(hrefs).to include("http://default.example", "http://foo.example") + expect(prefixes).to include(nil, "foo") + end + + it "exposes inherited namespaces via #namespaces" do + doc = Taurus::XML::Document.parse(<<~XML) + + + + XML + child = doc.root.first_element_child + expect(child.namespaces).to include("xmlns:foo" => "http://foo.example") + end +end + +RSpec.describe "v0.5.0+ line + compare" do + it "exposes #line for parsed nodes" do + doc = Taurus::XML::Document.parse("\n \n") + child = doc.root.first_element_child + # libtaurus v0.5.1 returns 0 for line numbers (function exists but + # underlying tracking is not populated); just verify the call works. + expect(child.line).to be_an(Integer) + end + + it "compares nodes by document order via #<=>" do + doc = Taurus::XML::Document.parse("") + a = doc.root.first_element_child + b = a.next_element + expect(a <=> b).to be < 0 + expect(b <=> a).to be > 0 + expect(a <=> a).to eq(0) + end +end + +RSpec.describe "v0.5.0+ serialization + C14N" do + let(:doc) { Taurus::XML::Document.parse("text") } + + it "serializes the document to XML via Document#to_xml" do + xml = doc.to_xml + expect(xml).to include("") + expect(xml).to include("") + expect(root_xml).not_to match(/\A<\?xml/) + end + + it "canonicalizes the whole document via Document#canonicalize" do + c14n = doc.canonicalize + expect(c14n).to include("") + expect(c14n).to include("") + end + + it "preserves C14N 1.0 vs 1.1 modes" do + c14n_10 = doc.canonicalize(Taurus::XML::FFI::C14N_1_0) + c14n_11 = doc.canonicalize(Taurus::XML::FFI::C14N_1_1) + expect(c14n_10).to include("") + end +end diff --git a/spec/xml/v060_features_spec.rb b/spec/xml/v060_features_spec.rb new file mode 100644 index 0000000..82d84ee --- /dev/null +++ b/spec/xml/v060_features_spec.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require "taurus/xml" + +RSpec.describe "v0.6.0+ element/document deep copy + node path + fragment + doctype" do + describe "Element#dup / Node#dup" do + let(:doc) { Taurus::XML::Document.parse("text") } + + it "creates a detached deep copy of an element subtree" do + a = doc.root.first_element_child + copy = a.dup + expect(copy).to be_a(Taurus::XML::Element) + expect(copy["x"]).to eq("1") + expect(copy.element_children.map(&:name)).to eq(%w[b]) + expect(copy.content).to eq("text") + end + + it "produces a copy whose pointer differs from the original" do + a = doc.root.first_element_child + copy = a.dup + expect(copy.c_ptr).not_to eq(a.c_ptr) + end + + it "modifications to the copy do not affect the original" do + a = doc.root.first_element_child + copy = a.dup + copy["x"] = "modified" + expect(a["x"]).to eq("1") + expect(copy["x"]).to eq("modified") + end + + it "can be appended to the same document as a new subtree" do + a = doc.root.first_element_child + copy = a.dup + doc.root.add_child(copy) + expect(doc.root.element_children.map(&:name)).to eq(%w[a c a]) + end + + it "alias #clone works the same" do + a = doc.root.first_element_child + expect(a.clone.content).to eq(a.content) + end + end + + describe "Document#dup / #clone" do + it "produces a deep copy of the whole document" do + doc = Taurus::XML::Document.parse("") + copy = doc.dup + expect(copy).to be_a(Taurus::XML::Document) + expect(copy.c_ptr).not_to eq(doc.c_ptr) + expect(copy.root.element_children.map(&:name)).to eq(%w[a]) + end + + it "mutations in the copy do not affect the original" do + doc = Taurus::XML::Document.parse("") + copy = doc.dup + copy.root.first_element_child.name = "changed" + expect(doc.root.first_element_child.name).to eq("a") + expect(copy.root.first_element_child.name).to eq("changed") + end + end + + describe "Node#path / #css_path" do + let(:doc) do + Taurus::XML::Document.parse("") + end + + it "returns the canonical XPath to a node" do + items = doc.xpath("//item") + path_of_second = items[1].path + expect(path_of_second).to include("item[2]") + end + + it "the root's path starts with /" do + expect(doc.root.path).to match(%r{\A/r}) + end + + it "Node#css_path translates [N] to :nth-of-type(N)" do + items = doc.xpath("//item") + css_path = items[1].css_path + expect(css_path).to include("item:nth-of-type(2)") + end + end + + describe "Document#fragment" do + it "parses a fragment with multiple top-level nodes" do + doc = Taurus::XML::Document.parse("") + frag = doc.fragment("") + expect(frag).to be_a(Taurus::XML::DocumentFragment) + expect(frag.children.map(&:name)).to eq(%w[a b c]) + end + + it "parses mixed content (elements + text + comment)" do + doc = Taurus::XML::Document.parse("") + frag = doc.fragment("hellox") + types = frag.children.map { |n| [n.class.name.split("::").last, n.respond_to?(:name) ? n.name : nil] } + expect(types.map(&:first)).to include("Text", "Element", "Comment") + end + end + + describe "Element#add_child with String markup" do + it "parses the markup and appends each top-level node" do + doc = Taurus::XML::Document.parse("") + result = doc.root.add_child("") + expect(result).to be_a(Taurus::XML::NodeSet) + expect(doc.root.element_children.map(&:name)).to eq(%w[a b]) + end + + it "parses mixed-content strings" do + doc = Taurus::XML::Document.parse("") + doc.root.add_child("hellox") + names = doc.root.children.map { |n| [n.class.name.split("::").last, n.respond_to?(:name) ? n.name : nil] } + expect(names.map(&:first)).to include("Text", "Element") + end + end + + describe "Document#doctype / #internal_subset" do + it "returns nil when the document has no DOCTYPE" do + doc = Taurus::XML::Document.parse("") + expect(doc.doctype).to be_nil + expect(doc.internal_subset).to be_nil + end + + it "exposes the DOCTYPE name (root element name)" do + doc = Taurus::XML::Document.parse(%q{}) + dt = doc.doctype + expect(dt).to be_a(Taurus::XML::DocType) + expect(dt.name).to eq("html") + expect(dt.root_name).to eq("html") + end + + it "exposes PUBLIC and SYSTEM identifiers" do + xml = <<~XML + + + XML + doc = Taurus::XML::Document.parse(xml) + dt = doc.doctype + expect(dt.public_id).to eq("-//W3C//DTD XHTML 1.0 Strict//EN") + expect(dt.system_id).to eq("http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd") + end + + it "exposes SYSTEM-only identifiers" do + xml = %q{} + doc = Taurus::XML::Document.parse(xml) + dt = doc.doctype + expect(dt.public_id).to be_nil + expect(dt.system_id).to eq("config.dtd") + end + + it "exposes the internal subset (DTD declarations)" do + xml = %q{]>} + doc = Taurus::XML::Document.parse(xml) + dt = doc.doctype + subset = dt.internal_subset + expect(subset).to include("} + doc = Taurus::XML::Document.parse(xml) + expect(doc.doctype.to_s) + .to include('PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"') + expect(doc.doctype.to_s).to include(" + Ruby + XML + XPath + + XML + end + + it "evaluates count() and returns a Float" do + expect(doc.xpath("count(//book)")).to eq(3.0) + end + + it "returns a NodeSet for //element queries" do + books = doc.xpath("//book") + expect(books).to be_a(Taurus::XML::NodeSet) + expect(books.length).to eq(3) + end + + it "returns a String for string() queries" do + title = doc.xpath("string(//book[@id='1']/title)") + expect(title).to eq("Ruby") + end + + it "returns a Boolean for predicate-only queries" do + # Uses boolean() which libtaurus v0.5.5+ flat XPath dispatcher + # correctly supports. + expect(doc.xpath("boolean(//book)")).to be true + expect(doc.xpath("boolean(//missing)")).to be false + end + + # Regression for upstream libtaurus #201 (fixed in v0.5.7): the v0.5.5 + # flat XPath dispatcher over-matched expressions starting with count(...), + # returning the inner number rather than evaluating the full comparison. + it "returns a Boolean for count(...) > N expressions" do + expect(doc.xpath("count(//book) > 0")).to be true + expect(doc.xpath("count(//missing) > 0")).to be false + expect(doc.xpath("count(//book) > 5")).to be false + expect(doc.xpath("count(//book) >= 3")).to be true + end + + it "supports at_xpath returning the first match" do + first_book = doc.at_xpath("//book") + expect(first_book["id"]).to eq("1") + end + + it "supports xpath on Element receiver (context-relative)" do + second_book = doc.at_xpath("//book[@id='2']") + title = second_book.xpath("title") + expect(title).to be_a(Taurus::XML::NodeSet) + expect(title.first.content).to eq("XML") + end + + it "supports . at context node" do + first_book = doc.at_xpath("//book") + expect(first_book.xpath(".")).to be_a(Taurus::XML::NodeSet) + end + + it "supports XPath on NodeSet (search each member)" do + books = doc.xpath("//book") + all_titles = books.xpath("title") + expect(all_titles.length).to eq(3) + expect(all_titles.map(&:content)).to eq(%w[Ruby XML XPath]) + end +end diff --git a/taurus.gemspec b/taurus.gemspec index 27b712d..8408acb 100644 --- a/taurus.gemspec +++ b/taurus.gemspec @@ -8,44 +8,35 @@ Gem::Specification.new do |spec| spec.authors = ["Ribose Inc."] spec.email = ["open.source@ribose.com"] - spec.summary = "Ultra-fast XML parser with full XPath support and CLI" + spec.summary = "Nokogiri-compatible Ruby binding for libtaurus (XML 1.0, XPath 1.0, SAX)" spec.description = <<~DESC - Taurus is a next-generation XML parser for Ruby with complete XPath 1.0 - support and command-line interface. Built in C for maximum performance, - it delivers Ox-level parsing speed with full namespace support and XPath - queries that are competitive with Nokogiri. Features: complete XPath 1.0 - (27 functions, 13 axes), XML pretty-printing CLI, zero external dependencies. + Taurus is a Nokogiri-compatible Ruby binding for libtaurus, a pure-C99 XML + 1.0 parser with full XPath 1.0 and SAX support. The C DOM is the single + source of truth; Ruby objects are thin FFI wrappers (one Ruby method = + one FFI call). DESC spec.homepage = "https://github.com/lutaml/taurus-ruby" spec.license = "MIT" spec.required_ruby_version = ">= 3.0.0" - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "https://github.com/lutaml/taurus-ruby" - spec.metadata["changelog_uri"] = "https://github.com/lutaml/taurus-ruby/blob/main/CHANGELOG.md" + spec.metadata = { + "homepage_uri" => spec.homepage, + "source_code_uri" => "https://github.com/lutaml/taurus-ruby", + "changelog_uri" => "https://github.com/lutaml/taurus-ruby/blob/main/CHANGELOG.md", + } - # Specify which files should be added to the gem when it is released. - # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir.chdir(__dir__) do `git ls-files -z`.split("\x0").reject do |f| (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) end end - spec.bindir = "bin" - spec.executables = ["taurus"] + spec.executables = [] spec.require_paths = ["lib"] - # Extension configuration - build libtaurus during gem install - spec.extensions = ["ext/taurus/extconf.rb"] - - # Runtime dependencies spec.add_dependency "ffi", "~> 1.15" - spec.add_dependency "thor", "~> 1.0" - # Development dependencies spec.add_development_dependency "rake" - spec.add_development_dependency "rake-compiler" spec.add_development_dependency "rspec" end