Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -183,38 +183,40 @@ private void startProcessElement(String qName, Attributes attrs) {
// 1 First, the local values are initialized
RDFaProcessingContext processingContext;
if (!this.processingContexts.isEmpty()) { // Not a root element
processingContext = new RDFaProcessingContext(currentProcessingContext().getEvaluationContext());
processingContext.setNamespaceDeclarations(currentProcessingContext().getNamespaceDeclarations());
processingContext.setRootElement(false);
RDFaProcessingContext parent = currentProcessingContext();
RDFaEvaluationContext childContext = new RDFaEvaluationContext(parent.getEvaluationContext());
// 13. Next, all elements that are children of the current element are processed using the rules described here, using a new evaluation context, initialized as follows:
// If the skip element flag is 'true' then the new evaluation context is a copy of the current context that was passed in to this level of processing, with the language and list of IRI mappings values replaced with the local values;
if (this.currentProcessingContext().isSkipElement()) {
processingContext.setEvaluationContext(new RDFaEvaluationContext(currentProcessingContext().getEvaluationContext()));
processingContext.getEvaluationContext().setLanguage(this.currentProcessingContext().getCurrentLanguage());
childContext.setLanguage(parent.getCurrentLanguage());
// Otherwise, the values are:
} else {
Resource oldParentSubject = currentProcessingContext().getEvaluationContext().getParentSubjectResource();
// the base is set to the base value of the current evaluation context;
processingContext.setEvaluationContext(new RDFaEvaluationContext(currentProcessingContext().getEvaluationContext()));
// the parent subject is set to the value of new subject, if non-null, or the value of the parent subject of the current evaluation context;
processingContext.getEvaluationContext().setParentSubjectResource(this.currentProcessingContext().getNewSubject());
childContext.setParentSubjectResource(parent.getNewSubject() != null
? parent.getNewSubject() : oldParentSubject);
// the parent object is set to value of current object resource, if non-null, or the value of new subject, if non-null, or the value of the parent subject of the current evaluation context;
if (this.currentProcessingContext().getCurrentObjectResource() != null) {
processingContext.getEvaluationContext().setParentObjectResource(this.currentProcessingContext().getCurrentObjectResource());
childContext.setParentObjectResource(parent.getCurrentObjectResource());
} else if (this.currentProcessingContext().getNewSubject() != null) {
processingContext.getEvaluationContext().setParentObjectResource(this.currentProcessingContext().getNewSubject());
childContext.setParentObjectResource(parent.getNewSubject());
} else {
processingContext.getEvaluationContext().setParentObjectResource(oldParentSubject);
childContext.setParentObjectResource(oldParentSubject);
}
// the list of incomplete triples is set to the local list of incomplete triples;
processingContext.getEvaluationContext().setIncompleteStatements(this.currentProcessingContext().getIncompleteStatements());
childContext.setIncompleteStatements(parent.getIncompleteStatements());
// the list mapping is set to the local list mapping;
processingContext.getEvaluationContext().setListMappings(this.currentProcessingContext().getListMappings());
childContext.setListMappings(parent.getChildListMappings());
// language is set to the value of current language.
processingContext.getEvaluationContext().setLanguage(this.currentProcessingContext().getCurrentLanguage());
childContext.setLanguage(parent.getCurrentLanguage());
// the default vocabulary is set to the value of the local default vocabulary.
processingContext.getEvaluationContext().setDefaultVocabulary(this.currentProcessingContext().getDefaultVocabulary());
childContext.setDefaultVocabulary(parent.getDefaultVocabulary());
}
// Initialize local references only after the incoming context is complete.
processingContext = new RDFaProcessingContext(childContext);
processingContext.setNamespaceDeclarations(parent.getNamespaceDeclarations());
processingContext.setRootElement(false);
} else {
// This is the start of the document
RDFaEvaluationContext startingContext = getNewContext(getValueFactory().createIRI(this.baseIri));
Expand Down Expand Up @@ -472,12 +474,12 @@ && isAttributePresent(RDFaAttributes.TYPEOF)) {
}
}

// 8. If in any of the previous steps a new subject was set to a non-null value different from the parent subject;
Resource parentSubj = currentProcessingContext().getEvaluationContext().getParentSubjectResource();
if (this.currentProcessingContext().getNewSubject() != null && !this.currentProcessingContext().getNewSubject().equals(parentSubj)) {
// 8. A subject different from the incoming parent object starts a local list scope.
Resource parentObject = currentProcessingContext().getEvaluationContext().getParentObjectResource();
if (this.currentProcessingContext().getNewSubject() != null && !this.currentProcessingContext().getNewSubject().equals(parentObject)) {
Map<IRI, List<Value>> freshListMappings = new HashMap<>();
this.currentProcessingContext().setListMappings(freshListMappings);
this.currentProcessingContext().getEvaluationContext().setListMappings(freshListMappings);
// Preserve the incoming map for incomplete relations and step 14 ownership.
}

// 9. If in any of the previous steps a current object resource was set to a non-null value, it is now used to generate triples and add entries to the local list mapping:
Expand Down Expand Up @@ -563,6 +565,14 @@ && isAttributePresent(RDFaAttributes.REL)) {
}
}
}

// 13. Descendants of a relation describe its object. Share one scope
// between those descendants, distinct from this element's subject lists.
RDFaProcessingContext context = currentProcessingContext();
if (!context.isSkipElement() && context.getCurrentObjectResource() != null
&& !context.getCurrentObjectResource().equals(context.getNewSubject())) {
context.setChildListMappings(new HashMap<>());
}
}

/*
Expand Down Expand Up @@ -691,37 +701,41 @@ private void endProcessElement(String qName) {
}

// 14. Finally, if there is one or more mapping in the local list mapping, list triples are generated as follows:
Resource parentSubj = currentProcessingContext().getEvaluationContext().getParentSubjectResource();
boolean isListOwner = this.currentProcessingContext().isRootElement()
|| parentSubj == null
|| (this.currentProcessingContext().getNewSubject() != null && !this.currentProcessingContext().getNewSubject().equals(parentSubj));

if (isListOwner) {
for (Map.Entry<IRI, List<Value>> listMapping : this.currentProcessingContext().getListMappings().entrySet()) {
IRI propertyIRI = listMapping.getKey();
List<Value> propertyList = listMapping.getValue();

if (propertyList.isEmpty()) {
getModel().add(this.currentProcessingContext().getNewSubject(), propertyIRI, RDF.nil.getIRI());
} else {
List<BNode> bnodes = new ArrayList<>();
for (int i = 0; i < propertyList.size(); i++) {
bnodes.add(getValueFactory().createBNode());
}
for (int i = 0; i < propertyList.size(); i++) {
BNode elementNode = bnodes.get(i);
Resource nextElementNode = (i < propertyList.size() - 1) ? bnodes.get(i + 1) : RDF.nil.getIRI();
getModel().add(elementNode, RDF.first.getIRI(), propertyList.get(i));
getModel().add(elementNode, RDF.rest.getIRI(), nextElementNode);
}
getModel().add(this.currentProcessingContext().getNewSubject(), propertyIRI, bnodes.getFirst());
}
}
RDFaProcessingContext context = currentProcessingContext();
if (context.getListMappings() != context.getEvaluationContext().getListMappings()) {
emitListMappings(context.getNewSubject(), context.getListMappings());
}
if (context.getChildListMappings() != context.getListMappings()) {
emitListMappings(context.getCurrentObjectResource(), context.getChildListMappings());
}

this.processingContexts.pop();
}

/** Emit each owned list scope once, after all descendants have contributed. */
private void emitListMappings(Resource subject, Map<IRI, List<Value>> mappings) {
for (Map.Entry<IRI, List<Value>> listMapping : mappings.entrySet()) {
IRI propertyIRI = listMapping.getKey();
List<Value> propertyList = listMapping.getValue();

if (propertyList.isEmpty()) {
getModel().add(subject, propertyIRI, RDF.nil.getIRI());
} else {
List<BNode> bnodes = new ArrayList<>();
for (int i = 0; i < propertyList.size(); i++) {
bnodes.add(getValueFactory().createBNode());
}
for (int i = 0; i < propertyList.size(); i++) {
BNode elementNode = bnodes.get(i);
Resource nextElementNode = (i < propertyList.size() - 1) ? bnodes.get(i + 1) : RDF.nil.getIRI();
getModel().add(elementNode, RDF.first.getIRI(), propertyList.get(i));
getModel().add(elementNode, RDF.rest.getIRI(), nextElementNode);
}
getModel().add(subject, propertyIRI, bnodes.getFirst());
}
}
}

/**
* Internal SAX handler that delegates to the parser's methods
*/
Expand Down Expand Up @@ -828,7 +842,7 @@ private void parseSinglePrefixDeclaration(String prefixRaw, String namespaceStri
throw new ParsingException("Expecting namespace prefix declaration to end with \":\", got " + prefixRaw + " in declaration " + declaration);
}
String prefix = prefixRaw.replaceAll(":$", "").trim();
if ((prefix.isEmpty() && !isHtmlDocument()) || prefix.startsWith("_")) {
if (prefix.isEmpty() || prefix.startsWith("_")) {
return;
}
if (namespaceString == null || namespaceString.trim().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class RDFaProcessingContext {
private Resource typedResource = null;
private Set<RDFaIncompleteStatement> incompleteStatements = null;
private Map<IRI, List<Value>> listMappings = new HashMap<>();
/** Separate list scope when descendants describe the current object. */
private Map<IRI, List<Value>> childListMappings;
private String currentLanguage = null;
private Value currentPropertyValue = null;
private String defaultVocabulary = null;
Expand Down Expand Up @@ -188,6 +190,14 @@ public void setListMappings(Map<IRI, List<Value>> listMappings) {
this.listMappings = listMappings;
}

public Map<IRI, List<Value>> getChildListMappings() {
return childListMappings != null ? childListMappings : listMappings;
}

public void setChildListMappings(Map<IRI, List<Value>> childListMappings) {
this.childListMappings = childListMappings;
}

/**
*
* @param key The IRI of the list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,116 @@ void getRDFFormat() {
assertEquals(RDFFormat.RDFA, parser.getRDFFormat());
}

@Test
void descendantsShareOneListOnTheParentObject() {
assertRdfaLists("""
<div about="ex:subject" rel="ex:link" resource="ex:object">
<span property="ex:items" inlist="">A</span>
<span property="ex:items" inlist="">B</span>
</div>
""", "ex:subject ex:link ex:object . ex:object ex:items (\"A\" \"B\") .");
}

@Test
void listSubjectAndRelationObjectKeepSeparateCollections() {
assertRdfaLists("""
<div about="ex:subject">
<span property="ex:items" inlist="">S1</span>
<div rel="ex:link" resource="ex:object">
<span property="ex:items" inlist="">O1</span>
<span property="ex:items" inlist="">O2</span>
</div>
<span property="ex:items" inlist="">S2</span>
</div>
""", """
ex:subject ex:link ex:object ; ex:items ("S1" "S2") .
ex:object ex:items ("O1" "O2") .
""");
}

@Test
void transparentWrappersAndRepeatedSubjectPreserveListOrder() {
assertRdfaLists("""
<div about="ex:subject" rel="ex:link" resource="ex:object">
<section><span property="ex:items" inlist="">A</span></section>
<section about="ex:object"><span property="ex:items" inlist="">B</span></section>
<span property="ex:items" inlist="">A</span>
</div>
""", "ex:subject ex:link ex:object . ex:object ex:items (\"A\" \"B\" \"A\") .");
}

@Test
void nestedSubjectDoesNotConsumeTheInheritedList() {
assertRdfaLists("""
<div about="ex:subject">
<span property="ex:items" inlist="">A</span>
<section about="ex:other"><span property="ex:items" inlist="">B</span></section>
<span property="ex:items" inlist="">C</span>
<span property="ex:otherItems" inlist="">D</span>
</div>
""", """
ex:subject ex:items ("A" "C") ; ex:otherItems ("D") .
ex:other ex:items ("B") .
""");
}

@Test
void incompleteListRelationCollectsDescendantsAndKeepsEmptyLists() {
assertRdfaLists("""
<div about="ex:subject">
<div rel="ex:items" inlist="">
<span about="ex:first"></span>
<section><span about="ex:second"></span></section>
</div>
<span rel="ex:empty" inlist=""></span>
</div>
""", "ex:subject ex:items (ex:first ex:second) ; ex:empty rdf:nil .");
}

@Test
void listRelationAndObjectPropertiesHaveDifferentOwners() {
assertRdfaLists("""
<div about="ex:subject" rel="ex:items" resource="ex:object" inlist="">
<span property="ex:items" inlist="">A</span>
<span property="ex:items" inlist="">B</span>
</div>
""", "ex:subject ex:items (ex:object) . ex:object ex:items (\"A\" \"B\") .");
}

private void assertRdfaLists(String body, String expectedTurtle) {
String document = "<html xmlns=\"http://www.w3.org/1999/xhtml\" prefix=\"ex: http://example.org/\"><body>"
+ body + "</body></html>";
Model actual = createTestModel();
RDFParser parser = parserFactory.createRDFParser(RDFFormat.RDFA, actual, valueFactory);
parser.parse(new ByteArrayInputStream(document.getBytes(java.nio.charset.StandardCharsets.UTF_8)),
"http://example.org/document");
Model expected = createTestModel();
RDFParser turtle = parserFactory.createRDFParser(RDFFormat.TURTLE, expected, valueFactory);
turtle.parse(new ByteArrayInputStream((defaultTurtlePrefixes + expectedTurtle)
.getBytes(java.nio.charset.StandardCharsets.UTF_8)), "http://example.org/document");
assertModelsIsomorphic(expected, actual);
}

@Test
void emptyPrefixDeclarationCannotOverrideTheDefaultRdfaPrefix() {
String xhtml = """
<html xmlns="http://www.w3.org/1999/xhtml" version="XHTML+RDFa 1.1">
<body about="http://example.org/person" prefix=": http://xmlns.com/foaf/0.1/">
<span property=":name">Ivan Herman</span>
</body>
</html>
""";
Model model = createTestModel();
RDFParser parser = parserFactory.createRDFParser(RDFFormat.RDFA, model, valueFactory);
parser.parse(new ByteArrayInputStream(xhtml.getBytes(java.nio.charset.StandardCharsets.UTF_8)),
"http://example.org/document");
IRI subject = valueFactory.createIRI("http://example.org/person");
Literal name = valueFactory.createLiteral("Ivan Herman");
assertEquals(1, model.size());
assertTrue(model.contains(subject, valueFactory.createIRI("http://www.w3.org/1999/xhtml/vocab#name"), name));
assertFalse(model.contains(subject, valueFactory.createIRI("http://xmlns.com/foaf/0.1/name"), name));
}

@Test
void parseCurrentSubjectCreatorHead() {
String currentSubjectXHTML = """
Expand Down
Loading