Skip to content

Commit cf1a677

Browse files
committed
Write text direction and triple terms.
1 parent 9d29d61 commit cf1a677

File tree

2 files changed

+188
-11
lines changed

2 files changed

+188
-11
lines changed

lib/rdf/rdfxml/writer.rb

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ class Writer < RDF::Writer
6363
# @return [RDF::URI] Base URI used for relativizing URIs
6464
attr_accessor :base_uri
6565

66+
# @return [String] RDF Version to output, if any
67+
attr_accessor :version
68+
69+
# @return [Boolean] Set to true if any literal includes a base direction
70+
attr_accessor :has_direction
71+
6672
##
6773
# RDF/XML Writer options
6874
# @see https://ruby-rdf.github.io/rdf/RDF/Writer#options-class_method
@@ -132,6 +138,9 @@ def initialize(output = $stdout, **options, &block)
132138
@uri_to_qname = {}
133139
@top_classes = options[:top_classes] || [RDF::RDFS.Class]
134140

141+
# FIXME: If version is specified in media type, use it to set an explicit version
142+
@version = nil
143+
135144
block.call(self) if block_given?
136145
end
137146
end
@@ -159,6 +168,7 @@ def write_epilogue
159168
log_debug {"\nserialize: graph size: #{@graph.size}"}
160169

161170
preprocess
171+
162172
# Prefixes
163173
prefix = prefixes.keys.map {|pk| "#{pk}: #{prefixes[pk]}"}.sort.join(" ") unless prefixes.empty?
164174
log_debug {"\nserialize: prefixes: #{prefix.inspect}"}
@@ -188,12 +198,12 @@ def reset
188198
@subjects = {}
189199
end
190200

191-
# Render document using `haml_template[:doc]`. Yields each subject to be rendered separately.
201+
# Render document. Yields each subject to be rendered separately.
192202
#
193203
# @param [Array<RDF::Resource>] subjects
194204
# Ordered list of subjects. Template must yield to each subject, which returns
195205
# the serialization of that subject (@see #subject_template)
196-
# @param [Hash{Symbol => Object}] options Rendering options passed to Haml render.
206+
# @param [Hash{Symbol => Object}] options Rendering options.
197207
# @option options [RDF::URI] base (nil)
198208
# Base URI added to document, used for shortening URIs within the document.
199209
# @option options [Symbol, String] language (nil)
@@ -203,8 +213,6 @@ def reset
203213
# Value of html>head>title element.
204214
# @option options [String] prefix (nil)
205215
# Value of @prefix attribute.
206-
# @option options [String] haml (haml_template[:doc])
207-
# Haml template to render.
208216
# @yield [subject]
209217
# Yields each subject
210218
# @yieldparam [RDF::URI] subject
@@ -219,6 +227,8 @@ def render_document(subjects, lang: nil, base: nil, **options, &block)
219227
attrs = prefix_attrs
220228
attrs[:"xml:lang"] = lang if lang
221229
attrs[:"xml:base"] = base if base
230+
attrs[:"rdf:version"] = version.freeze if version
231+
attrs[:"its:version"] = "2.0" if has_direction
222232

223233
builder.rdf(:RDF, **attrs) do |b|
224234
subjects.each do |subject|
@@ -227,18 +237,18 @@ def render_document(subjects, lang: nil, base: nil, **options, &block)
227237
end
228238
end
229239

230-
# Render a subject using `haml_template[:subject]`.
240+
# Render a subject.
231241
#
232242
# The _subject_ template may be called either as a top-level element, or recursively under another element if the _rel_ local is not nil.
233243
#
234-
# For RDF/XML, removes from predicates those that can be rendered as attributes, and adds the `:attr_props` local for the Haml template, which includes all attributes to be rendered as properties.
244+
# For RDF/XML, removes from predicates those that can be rendered as attributes, and adds the `:attr_props` local, which includes all attributes to be rendered as properties.
235245
#
236246
# Yields each property to be rendered separately.
237247
#
238248
# @param [Array<RDF::Resource>] subject
239249
# Subject to render
240250
# @param [Builder::RdfXml] builder
241-
# @param [Hash{Symbol => Object}] options Rendering options passed to Haml render.
251+
# @param [Hash{Symbol => Object}] options Rendering options passed to builder.
242252
# @option options [String] about (nil)
243253
# About description, a QName, URI or Node definition.
244254
# May be nil if no @about is rendered (e.g. unreferenced Nodes)
@@ -252,8 +262,6 @@ def render_document(subjects, lang: nil, base: nil, **options, &block)
252262
# If :about is nil, this defaults to the empty string ("").
253263
# @option options [:li, nil] element (nil)
254264
# Render with &lt;li&gt;, otherwise with template default.
255-
# @option options [String] haml (haml_template[:subject])
256-
# Haml template to render.
257265
# @yield [predicate]
258266
# Yields each predicate
259267
# @yieldparam [RDF::URI] predicate
@@ -303,9 +311,10 @@ def render_subject(subject, builder, **options, &block)
303311
# @param [Array<RDF::Resource>] objects
304312
# List of objects to render. If the list contains only a single element, the :property_value template will be used. Otherwise, the :property_values template is used.
305313
# @param [Builder::RdfXml] builder
306-
# @param [Hash{Symbol => Object}] options Rendering options passed to Haml render.
314+
# @param [Hash{Symbol => Object}] options Rendering options.
307315
def render_property(property, objects, builder, **options)
308316
log_debug {"render_property(#{property}): #{objects.inspect}"}
317+
property = get_qname(property) if property.is_a?(RDF::URI)
309318

310319
# Separate out the objects which are lists and render separately
311320
lists = objects.
@@ -351,7 +360,12 @@ def render_property(property, objects, builder, **options)
351360
attrs = {}
352361
attrs[:"xml:lang"] = object.language if object.language?
353362
attrs[:"rdf:datatype"] = object.datatype if object.datatype?
363+
attrs[:"its:dir"] = object.direction if object.direction?
354364
builder.tag!(property, object.value.to_s, **attrs)
365+
elsif object.statement?
366+
builder.tag!(property, "rdf:parseType": "Triple") do |b|
367+
render_triple_term(object, b, **options)
368+
end
355369
elsif object.node?
356370
builder.tag!(property, "rdf:nodeID": object.id)
357371
else
@@ -367,6 +381,18 @@ def render_property(property, objects, builder, **options)
367381
end
368382
end
369383

384+
##
385+
# Render a triple term, which may be recursive
386+
def render_triple_term(term, builder, **options)
387+
attr_props = {}
388+
attr_props = attr_props.merge("rdf:nodeID": term.subject.id) if term.subject.node?
389+
attr_props = attr_props.merge("rdf:about": term.subject.relativize(base_uri)) if term.subject.uri?
390+
391+
builder.tag!("rdf:Description", **attr_props) do |b|
392+
render_property(term.predicate, [term.object], b)
393+
end
394+
end
395+
370396
##
371397
# Render a collection, which may be included in a property declaration, or
372398
# may be recursive within another collection
@@ -441,6 +467,28 @@ def preprocess_statement(statement)
441467
ensure_qname(statement.predicate)
442468
statement.predicate == RDF.type && statement.object.uri? ? ensure_qname(statement.object) : get_qname(statement.object)
443469
get_qname(statement.object.datatype) if statement.object.literal? && statement.object.datatype?
470+
471+
# Base direction requires a prefix, used to set the its:version in the document
472+
if statement.object.literal? && statement.object.direction?
473+
prefix(:its, RDF::ITS.to_s)
474+
@has_direction = true # Indirectly adds its:version to document element
475+
476+
# It's an error if version is frozen and not at least "1.2-basic"
477+
if version && version.frozen?
478+
log_error("Literal direction is incompatible with required version #{version}: #{statement.object.direction}") if
479+
version == "1.1"
480+
elsif version.nil?
481+
@version = "1.2-basic"
482+
end
483+
elsif statement.object.statement?
484+
# It's an error if version is frozen and not at least "1.2-basic"
485+
if version && version.frozen?
486+
log_error("Triple terms are incompatible with required version #{version}") if
487+
version != "1.2"
488+
else
489+
@version = "1.2"
490+
end
491+
end
444492
end
445493

446494
private
@@ -583,7 +631,7 @@ def get_qname(resource)
583631
nil
584632
end
585633
when RDF::Node then resource.to_s
586-
when RDF::Literal then nil
634+
when RDF::Literal, RDF::Statement then nil
587635
else
588636
log_error("Getting QName for #{resource.inspect}, which must be a resource")
589637
nil

spec/writer_spec.rb

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class FOO < RDF::Vocabulary("http://foo/"); end
77

88
describe "RDF::RDFXML::Writer" do
99
let(:logger) {RDF::Spec.logger}
10+
after(:each) {|example| puts logger.to_s if example.exception}
11+
1012
it_behaves_like 'an RDF::Writer' do
1113
let(:writer) {RDF::RDFXML::Writer.new(::StringIO.new)}
1214
end
@@ -635,6 +637,133 @@ class FOO < RDF::Vocabulary("http://foo/"); end
635637
end
636638
end
637639

640+
context "base direction" do
641+
{
642+
"base direction ltr": {
643+
input: %(<http://example/a> <http://example/b> "Hello"@en--ltr .),
644+
xpath: {
645+
"/rdf:RDF/@rdf:version" => "1.2-basic",
646+
"/rdf:RDF/@its:version" => "2.0",
647+
"/rdf:RDF/rdf:Description/ns0:b/@xml:lang" => "en",
648+
"/rdf:RDF/rdf:Description/ns0:b/@its:dir" => "ltr",
649+
"/rdf:RDF/rdf:Description/ns0:b/text()" => "Hello",
650+
}
651+
},
652+
"base direction rtl": {
653+
input: %(<http://example/a> <http://example/b> "Hello"@en--rtl .),
654+
xpath: {
655+
"/rdf:RDF/@rdf:version" => "1.2-basic",
656+
"/rdf:RDF/@its:version" => "2.0",
657+
"/rdf:RDF/rdf:Description/ns0:b/@xml:lang" => "en",
658+
"/rdf:RDF/rdf:Description/ns0:b/@its:dir" => "rtl",
659+
"/rdf:RDF/rdf:Description/ns0:b/text()" => "Hello",
660+
}
661+
},
662+
"unknown base direction": {
663+
input: %(<http://example/a> <http://example/b> "Hello"@en--unk .),
664+
exception: RDF::WriterError
665+
},
666+
"base direction LTR": {
667+
input: %(<http://example/a> <http://example/b> "Hello"@en--LTR .),
668+
exception: RDF::WriterError
669+
}
670+
}.each do |name, params|
671+
context name do
672+
let!(:graph) {RDF::Graph.new {|g| g << parse(params[:input], rdfstar: true, format: :ntriples)}}
673+
subject {serialize(graph)}
674+
675+
if params[:exception]
676+
it "raises error" do
677+
expect {
678+
serialize(graph, validate: true)
679+
}.to raise_error(params[:exception])
680+
end
681+
else
682+
it "generates equivalent graph" do
683+
doc = parse(subject)
684+
expect(doc).to be_equivalent_graph(graph, logger: logger)
685+
end
686+
end
687+
688+
params.fetch(:xpath, {}).each do |path, value|
689+
it "returns #{value.inspect} for xpath #{path}" do
690+
expect(subject).to have_xpath(path, value, {}, logger)
691+
end
692+
end
693+
end
694+
end
695+
end
696+
697+
context "triple terms" do
698+
{
699+
"object-iii": {
700+
input: %(<http://example/s> <http://example/p> <<(<http://example/s1> <http://example/p1> <http://example/o1>)>> .),
701+
xpath: {
702+
"/rdf:RDF/@rdf:version" => "1.2",
703+
"/rdf:RDF/rdf:Description/ns0:p/@rdf:parseType" => "Triple",
704+
"/rdf:RDF/rdf:Description/ns0:p/rdf:Description" => true,
705+
"/rdf:RDF/rdf:Description/ns0:p/rdf:Description/ns0:p1" => true,
706+
}
707+
},
708+
"object-iib": {
709+
input: %(<http://example/s> <http://example/p> <<(<http://example/s1> <http://example/p1> _:o1)>> .),
710+
xpath: {
711+
"/rdf:RDF/@rdf:version" => "1.2",
712+
"/rdf:RDF/rdf:Description/ns0:p/@rdf:parseType" => "Triple",
713+
"/rdf:RDF/rdf:Description/ns0:p/rdf:Description" => true,
714+
"/rdf:RDF/rdf:Description/ns0:p/rdf:Description/ns0:p1" => true,
715+
}
716+
},
717+
"object-iil": {
718+
input: %(<http://example/s> <http://example/p> <<(<http://example/s1> <http://example/p1> "o1")>> .),
719+
xpath: {
720+
"/rdf:RDF/@rdf:version" => "1.2",
721+
"/rdf:RDF/rdf:Description/ns0:p/@rdf:parseType" => "Triple",
722+
"/rdf:RDF/rdf:Description/ns0:p/rdf:Description" => true,
723+
"/rdf:RDF/rdf:Description/ns0:p/rdf:Description/ns0:p1" => true,
724+
}
725+
},
726+
"recursive-object": {
727+
input: %(<http://example/s> <http://example/p> <<(<http://example/s1> <http://example/p1> <<(<http://example/s2> <http://example/p2> <http://example/o2>)>>)>> .),
728+
xpath: {
729+
"/rdf:RDF/@rdf:version" => "1.2",
730+
"/rdf:RDF/rdf:Description/ns0:p/@rdf:parseType" => "Triple",
731+
"/rdf:RDF/rdf:Description/ns0:p/rdf:Description" => true,
732+
"/rdf:RDF/rdf:Description/ns0:p/rdf:Description/ns0:p1" => true,
733+
}
734+
},
735+
"two-objects": {
736+
input: %(
737+
<http://example/s> <http://example/p> <<(<http://example/s1> <http://example/p1> <http://example/o1>)>> .
738+
<http://example/s> <http://example/p> <<(<http://example/s2> <http://example/p2> <http://example/o2>)>> .
739+
),
740+
xpath: {
741+
"/rdf:RDF/@rdf:version" => "1.2",
742+
"/rdf:RDF/rdf:Description/ns0:p/@rdf:parseType" => "Triple",
743+
"/rdf:RDF/rdf:Description/ns0:p/rdf:Description" => true,
744+
"/rdf:RDF/rdf:Description/ns0:p/rdf:Description/ns0:p1" => true,
745+
"/rdf:RDF/rdf:Description/ns0:p/rdf:Description/ns0:p2" => true,
746+
}
747+
}
748+
}.each do |name, params|
749+
context name do
750+
let!(:graph) {RDF::Graph.new {|g| g << parse(params[:input], rdfstar: true, format: :ntriples)}}
751+
subject {serialize(graph)}
752+
753+
it "generates equivalent graph" do
754+
doc = parse(subject)
755+
expect(doc).to be_equivalent_graph(graph, logger: logger)
756+
end
757+
758+
params.fetch(:xpath, {}).each do |path, value|
759+
it "returns #{value.inspect} for xpath #{path}" do
760+
expect(subject).to have_xpath(path, value, {}, logger)
761+
end
762+
end
763+
end
764+
end
765+
end
766+
638767
describe "with a stylesheet" do
639768
subject do
640769
nt = %(

0 commit comments

Comments
 (0)