From f82e031db077d9bebf13c078ed285f32ccd5effa Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Thu, 12 Jun 2025 13:35:35 +0200 Subject: [PATCH 01/13] add failing literal relationship test --- test-app/tests/unit/rdfa/rdfa-parsing-test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test-app/tests/unit/rdfa/rdfa-parsing-test.ts b/test-app/tests/unit/rdfa/rdfa-parsing-test.ts index 684163eb0..e50909103 100644 --- a/test-app/tests/unit/rdfa/rdfa-parsing-test.ts +++ b/test-app/tests/unit/rdfa/rdfa-parsing-test.ts @@ -690,4 +690,47 @@ module('rdfa | parsing', function () { const secondState = controller.mainEditorState.doc; assert.propEqual(secondState.toJSON(), initialState.toJSON()); }); + test('Literal relationships with the same subject and predicate are correctly parsed', function (assert) { + const { doc, block_rdfa, paragraph } = testBuilders; + const df = new SayDataFactory(); + const initialDocument = doc( + {}, + block_rdfa( + { + rdfaNodeType: 'resource', + subject: 'http://test/1', + __rdfaId: 'id1', + properties: [ + { + predicate: 'http://testPred', + object: df.literal('Inline literal'), + }, + { predicate: 'http://testPred', object: df.literalNode('id2') }, + ] satisfies OutgoingTriple[], + backlinks: [] satisfies IncomingTriple[], + }, + paragraph('value'), + ), + block_rdfa( + { + rdfaNodeType: 'literal', + __rdfaId: 'id2', + backlinks: [ + { + subject: df.resourceNode('http://test/1'), + predicate: 'http://testPred', + }, + ] satisfies IncomingTriple[], + }, + paragraph('Literal'), + ), + ); + QUnit.dump.maxDepth = 10; + const state = EditorState.create({ schema, plugins, doc: initialDocument }); + const { controller } = testEditor(schema, plugins, state); + const initialRender = controller.htmlContent; + controller.initialize(initialRender); + const resultingDocument = controller.mainEditorState.doc; + assert.propEqual(resultingDocument.toJSON(), initialDocument.toJSON()); + }); }); From 55d2ef036d8505f5dd6473eb8efffe5637dcf62e Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Thu, 12 Jun 2025 11:35:29 +0200 Subject: [PATCH 02/13] first attempt at fixing literal relationship bug --- .../src/core/rdfa-processor.ts | 20 +++++++++++------ .../src/utils/_private/datastore/node-map.ts | 22 ++++++++++++++----- .../utils/_private/rdfa-parser/rdfa-parser.ts | 20 ++++++++++++----- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/packages/ember-rdfa-editor/src/core/rdfa-processor.ts b/packages/ember-rdfa-editor/src/core/rdfa-processor.ts index 9dcde9af5..ad675b0d6 100644 --- a/packages/ember-rdfa-editor/src/core/rdfa-processor.ts +++ b/packages/ember-rdfa-editor/src/core/rdfa-processor.ts @@ -8,7 +8,7 @@ import { type default as Datastore, EditorStore, } from '#root/utils/_private/datastore/datastore.ts'; -import type { Quad } from '@rdfjs/types'; +import type { NamedNode, Quad } from '@rdfjs/types'; import { SayLiteral, SayNamedNode, @@ -196,11 +196,12 @@ export function preprocessRDFa(dom: Node, pathFromRoot?: Node[]) { for (const [node, object] of datastore.getContentNodeMap().entries()) { const { subject, predicate } = object; - const incomingProp = { + const incomingProp: IncomingTriple = { + // @ts-expect-error the incorrect termtype seems to be used here subject, - predicate, + predicate: predicate.value, }; - const extraBacklinks = []; + const extraBacklinks: IncomingTriple[] = []; if (isElement(node)) { const firstChild = node.firstElementChild; if ( @@ -215,7 +216,11 @@ export function preprocessRDFa(dom: Node, pathFromRoot?: Node[]) { ) { const backlink = datastore.getContentNodeMap().get(child); if (backlink) { - extraBacklinks.push(backlink); + extraBacklinks.push({ + // @ts-expect-error the incorrect termtype seems to be used here + subject: backlink.subject, + predicate: backlink.predicate.value, + }); } } } @@ -240,8 +245,9 @@ function quadToProperties( // check if quad refers to a contentNode if (quad.object.termType === 'Literal') { const contentNodes = datastore.getContentNodeMap().getValues({ - subject: sayDataFactory.resourceNode(quad.subject.value), - predicate: quad.predicate.value, + subject: quad.subject as NamedNode, + predicate: quad.predicate as NamedNode, + object: quad.object, }); if (contentNodes) { for (const contentNode of contentNodes) { diff --git a/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts b/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts index b62e1438c..ba66e5ab4 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts @@ -1,6 +1,6 @@ import type { Quad_Subject, Quad_Predicate, NamedNode } from '@rdfjs/types'; import { TwoWayMap } from '../map-utils.ts'; -import type { IncomingTriple } from '#root/core/rdfa-processor.ts'; +import type { Literal } from 'rdf-data-factory'; export interface SubAndContentPred { subject: Quad_Subject; @@ -8,8 +8,18 @@ export interface SubAndContentPred { contentDatatype?: NamedNode; contentLanguage?: string; } +type RdfaContentNodeMapEntry = { + subject: NamedNode; + predicate: NamedNode; + object: Literal; +}; export type RdfaResourceNodeMap = TwoWayMap; -export type RdfaContentNodeMap = TwoWayMap; +export type RdfaContentNodeMap = TwoWayMap< + N, + RdfaContentNodeMapEntry, + N, + string +>; export function rdfaResourceNodeMap( init?: Iterable<[N, SubAndContentPred]>, ): RdfaResourceNodeMap { @@ -19,11 +29,11 @@ export function rdfaResourceNodeMap( }); } export function rdfaContentNodeMap( - init?: Iterable<[N, IncomingTriple]>, + init?: Iterable<[N, RdfaContentNodeMapEntry]>, ): RdfaContentNodeMap { - return TwoWayMap.withValueStringHashing({ - valueHasher: ({ subject: { termType, value }, predicate }) => { - return `${termType}-${value}-${predicate}`; + return TwoWayMap.withValueStringHashing({ + valueHasher: ({ subject, predicate, object }) => { + return `${subject.value}__${predicate.value}__${object.value}${object.language ? `@${object.language}` : ''}^^${object.datatype.value}`; }, init, }); diff --git a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts index f8142cc35..731f6e4db 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts @@ -29,7 +29,10 @@ import { rdfaResourceNodeMap, } from '../datastore/node-map.ts'; import { postProcessTagAsRdfaNode } from './post-process-as-rdfa-nodes.ts'; -import { sayDataFactory } from '#root/core/say-data-factory/index.ts'; +import { + languageOrDataType, + sayDataFactory, +} from '#root/core/say-data-factory/index.ts'; import { LANG_STRING } from '../constants.ts'; export type ModelTerm = @@ -985,8 +988,12 @@ export class RdfaParser { attributes: Record, predicateAttribute = 'property', ) => { + // TODO: the issue here is that `activeTag.text` is not yet (fully) populated. + // This function is called during the `onTagOpen` phase, while the `activeTag.text` attribute is only fully populated during the `onTagClose` phase. + // This means we need to adjust this logic to ensure the `activeTag.text` attribute is fully populated before this function is called. + const content = attributes['content'] ?? activeTag.text?.join('') ?? ''; this.contentNodeMapping.set(node, { - subject: sayDataFactory.resourceNode( + subject: sayDataFactory.namedNode( this.util.getResourceOrBaseIri(unwrap(activeTag.subject), activeTag) .value, ), @@ -997,7 +1004,11 @@ export class RdfaParser { true, true, false, - ).value, + ), + object: sayDataFactory.literal( + content, + languageOrDataType(activeTag.language, activeTag.datatype), + ), }); }; @@ -1399,7 +1410,6 @@ export class RdfaParser { pattern: IRdfaPattern, root: boolean, rootPatternId: string, - node?: N, ) { // Stop on detection of cyclic patterns if ( @@ -1410,7 +1420,7 @@ export class RdfaParser { return; } - this.onTagOpen(pattern.name, pattern.attributes, node); + this.onTagOpen(pattern.name, pattern.attributes); for (const text of pattern.text) { this.onText(text); } From fc68ff4a21eb893e24bb12e78eb1c8cc9d6c8335 Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Thu, 12 Jun 2025 11:33:02 +0200 Subject: [PATCH 03/13] move rdfa-postprocessing to onTagClose --- .../src/utils/_private/datastore/node-map.ts | 2 +- .../utils/_private/rdfa-parser/active-tag.ts | 2 + .../utils/_private/rdfa-parser/rdfa-parser.ts | 40 +++++++++---------- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts b/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts index ba66e5ab4..1a6d478b7 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts @@ -33,7 +33,7 @@ export function rdfaContentNodeMap( ): RdfaContentNodeMap { return TwoWayMap.withValueStringHashing({ valueHasher: ({ subject, predicate, object }) => { - return `${subject.value}__${predicate.value}__${object.value}${object.language ? `@${object.language}` : ''}^^${object.datatype.value}`; + return `${subject.value}__${predicate.value}__${object.value}${object.language ? `@${object.language.toLowerCase()}` : ''}^^${object.datatype.value}`; }, init, }); diff --git a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/active-tag.ts b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/active-tag.ts index 4ba5036a3..43f32391e 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/active-tag.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/active-tag.ts @@ -16,6 +16,8 @@ import type { */ export interface IActiveTag { name: string; + attributes: Record; + typedResource?: ModelNamedNode | ModelBlankNode | true | null; prefixesAll: Record; prefixesCustom: Record; subject?: ModelNamedNode | ModelBlankNode | boolean; diff --git a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts index 731f6e4db..d3ed321f5 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts @@ -29,10 +29,7 @@ import { rdfaResourceNodeMap, } from '../datastore/node-map.ts'; import { postProcessTagAsRdfaNode } from './post-process-as-rdfa-nodes.ts'; -import { - languageOrDataType, - sayDataFactory, -} from '#root/core/say-data-factory/index.ts'; +import { sayDataFactory } from '#root/core/say-data-factory/index.ts'; import { LANG_STRING } from '../constants.ts'; export type ModelTerm = @@ -186,6 +183,7 @@ export class RdfaParser { listMapping: {}, listMappingLocal: {}, name: '', + attributes: {}, prefixesAll: { ...INITIAL_CONTEXT['@context'], ...(this.features.xhtmlInitialContext @@ -285,6 +283,7 @@ export class RdfaParser { listMappingLocal: parentTag.listMapping, localBaseIRI: parentTag.localBaseIRI, name, + attributes, prefixesAll: {}, prefixesCustom: {}, skipElement: false, @@ -971,15 +970,7 @@ export class RdfaParser { // 13: Save evaluation context into active tag activeTag.subject = newSubject || parentTag.subject; activeTag.object = currentObjectResource || newSubject; - - postProcessTagAsRdfaNode({ - activeTag, - attributes, - isRootTag, - typedResource, - markAsLiteralNode: this.markAsLiteralNode, - markAsResourceNode: this.markAsResourceNode, - }); + activeTag.typedResource = typedResource; } markAsLiteralNode = ( @@ -988,10 +979,8 @@ export class RdfaParser { attributes: Record, predicateAttribute = 'property', ) => { - // TODO: the issue here is that `activeTag.text` is not yet (fully) populated. - // This function is called during the `onTagOpen` phase, while the `activeTag.text` attribute is only fully populated during the `onTagClose` phase. - // This means we need to adjust this logic to ensure the `activeTag.text` attribute is fully populated before this function is called. - const content = attributes['content'] ?? activeTag.text?.join('') ?? ''; + const textSegments: string[] = activeTag.text || []; + const object = this.util.createLiteral(textSegments.join(''), activeTag); this.contentNodeMapping.set(node, { subject: sayDataFactory.namedNode( this.util.getResourceOrBaseIri(unwrap(activeTag.subject), activeTag) @@ -1005,10 +994,7 @@ export class RdfaParser { true, false, ), - object: sayDataFactory.literal( - content, - languageOrDataType(activeTag.language, activeTag.datatype), - ), + object, }); }; @@ -1131,7 +1117,8 @@ export class RdfaParser { // Reset text, unless the parent is also collecting text if (!parentTag.predicates) { - activeTag.text = null; + // Can we simply remove this statement? + // activeTag.text = null; } } @@ -1210,6 +1197,15 @@ export class RdfaParser { parentTag.text = parentTag.text.concat(activeTag.text); } } + const isRootTag: boolean = this.activeTagStack.length === 1; + postProcessTagAsRdfaNode({ + activeTag, + attributes: activeTag.attributes, + isRootTag, + typedResource: activeTag.typedResource ?? null, + markAsLiteralNode: this.markAsLiteralNode, + markAsResourceNode: this.markAsResourceNode, + }); } public onEnd() { From 2ffa1a3f0fb76fd597c5088fa556f835cc2ee551 Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Thu, 12 Jun 2025 14:07:35 +0200 Subject: [PATCH 04/13] named-node should be resource-node --- packages/ember-rdfa-editor/src/core/rdfa-processor.ts | 4 +--- .../src/utils/_private/datastore/node-map.ts | 3 ++- .../src/utils/_private/rdfa-parser/rdfa-parser.ts | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/ember-rdfa-editor/src/core/rdfa-processor.ts b/packages/ember-rdfa-editor/src/core/rdfa-processor.ts index ad675b0d6..f6f576c14 100644 --- a/packages/ember-rdfa-editor/src/core/rdfa-processor.ts +++ b/packages/ember-rdfa-editor/src/core/rdfa-processor.ts @@ -197,7 +197,6 @@ export function preprocessRDFa(dom: Node, pathFromRoot?: Node[]) { const { subject, predicate } = object; const incomingProp: IncomingTriple = { - // @ts-expect-error the incorrect termtype seems to be used here subject, predicate: predicate.value, }; @@ -217,7 +216,6 @@ export function preprocessRDFa(dom: Node, pathFromRoot?: Node[]) { const backlink = datastore.getContentNodeMap().get(child); if (backlink) { extraBacklinks.push({ - // @ts-expect-error the incorrect termtype seems to be used here subject: backlink.subject, predicate: backlink.predicate.value, }); @@ -245,7 +243,7 @@ function quadToProperties( // check if quad refers to a contentNode if (quad.object.termType === 'Literal') { const contentNodes = datastore.getContentNodeMap().getValues({ - subject: quad.subject as NamedNode, + subject: sayDataFactory.resourceNode(quad.subject.value), predicate: quad.predicate as NamedNode, object: quad.object, }); diff --git a/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts b/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts index 1a6d478b7..3f61714fa 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/datastore/node-map.ts @@ -1,6 +1,7 @@ import type { Quad_Subject, Quad_Predicate, NamedNode } from '@rdfjs/types'; import { TwoWayMap } from '../map-utils.ts'; import type { Literal } from 'rdf-data-factory'; +import type { ResourceNodeTerm } from '#root/core/say-data-factory/index.js'; export interface SubAndContentPred { subject: Quad_Subject; @@ -9,7 +10,7 @@ export interface SubAndContentPred { contentLanguage?: string; } type RdfaContentNodeMapEntry = { - subject: NamedNode; + subject: ResourceNodeTerm; predicate: NamedNode; object: Literal; }; diff --git a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts index d3ed321f5..42c566ad0 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts @@ -982,7 +982,7 @@ export class RdfaParser { const textSegments: string[] = activeTag.text || []; const object = this.util.createLiteral(textSegments.join(''), activeTag); this.contentNodeMapping.set(node, { - subject: sayDataFactory.namedNode( + subject: sayDataFactory.resourceNode( this.util.getResourceOrBaseIri(unwrap(activeTag.subject), activeTag) .value, ), From b0a3dd925958298e7c5908d7461b49fd498ca7dd Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Thu, 12 Jun 2025 14:18:32 +0200 Subject: [PATCH 05/13] contentNodeMap: take content attr into account --- .../src/utils/_private/rdfa-parser/rdfa-parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts index 42c566ad0..c3cdb4706 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts @@ -980,7 +980,7 @@ export class RdfaParser { predicateAttribute = 'property', ) => { const textSegments: string[] = activeTag.text || []; - const object = this.util.createLiteral(textSegments.join(''), activeTag); + const object = this.util.createLiteral(attributes['content'] ?? textSegments.join(''), activeTag); this.contentNodeMapping.set(node, { subject: sayDataFactory.resourceNode( this.util.getResourceOrBaseIri(unwrap(activeTag.subject), activeTag) From 9763eead86af10d54bea88c8e329bc745c329bbf Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Thu, 12 Jun 2025 14:23:42 +0200 Subject: [PATCH 06/13] linting --- .../src/utils/_private/rdfa-parser/rdfa-parser.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts index c3cdb4706..5bfc98027 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts @@ -980,7 +980,10 @@ export class RdfaParser { predicateAttribute = 'property', ) => { const textSegments: string[] = activeTag.text || []; - const object = this.util.createLiteral(attributes['content'] ?? textSegments.join(''), activeTag); + const object = this.util.createLiteral( + attributes['content'] ?? textSegments.join(''), + activeTag, + ); this.contentNodeMapping.set(node, { subject: sayDataFactory.resourceNode( this.util.getResourceOrBaseIri(unwrap(activeTag.subject), activeTag) From bf58c4f8f6e611d72488f659cd22dec13aaaea81 Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Thu, 12 Jun 2025 14:24:45 +0200 Subject: [PATCH 07/13] add changeset --- .changeset/thin-hoops-itch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thin-hoops-itch.md diff --git a/.changeset/thin-hoops-itch.md b/.changeset/thin-hoops-itch.md new file mode 100644 index 000000000..65fc5f9c9 --- /dev/null +++ b/.changeset/thin-hoops-itch.md @@ -0,0 +1,5 @@ +--- +'@lblod/ember-rdfa-editor': patch +--- + +rdfa-parser: fix issue where two relationships with the same subject and predicate pointing to literal-nodes/literals was not correctly parsed From a803ce20eb1bc9f60640567785c9600bea2310ca Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Fri, 13 Jun 2025 10:13:32 +0200 Subject: [PATCH 08/13] introduce seperate content property on activeTag --- .../utils/_private/rdfa-parser/active-tag.ts | 1 + .../utils/_private/rdfa-parser/rdfa-parser.ts | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/active-tag.ts b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/active-tag.ts index 43f32391e..557692a55 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/active-tag.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/active-tag.ts @@ -25,6 +25,7 @@ export interface IActiveTag { predicates?: ModelNamedNode[] | null; object?: ModelNamedNode | ModelBlankNode | boolean | null; text?: string[] | null; + content?: string | null; // contains `text` concatenated, or the `content` attribute vocab?: string; language?: string; datatype?: ModelNamedNode; diff --git a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts index 5bfc98027..de7956051 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/rdfa-parser.ts @@ -979,11 +979,7 @@ export class RdfaParser { attributes: Record, predicateAttribute = 'property', ) => { - const textSegments: string[] = activeTag.text || []; - const object = this.util.createLiteral( - attributes['content'] ?? textSegments.join(''), - activeTag, - ); + const object = this.util.createLiteral(activeTag.content ?? '', activeTag); this.contentNodeMapping.set(node, { subject: sayDataFactory.resourceNode( this.util.getResourceOrBaseIri(unwrap(activeTag.subject), activeTag) @@ -1049,6 +1045,14 @@ export class RdfaParser { const parentTag: IActiveTag = this.activeTagStack[this.activeTagStack.length - 2]; + let textSegments: string[] = activeTag.text || []; + if (activeTag.collectChildTags && parentTag.collectChildTags) { + // If we are inside an XMLLiteral child that also has RDFa content, ignore the tag name that was collected. + textSegments = textSegments.slice(1); + } + activeTag.content = + activeTag.attributes['content'] ?? textSegments.join(''); + if ( !( activeTag.collectChildTags && @@ -1120,8 +1124,7 @@ export class RdfaParser { // Reset text, unless the parent is also collecting text if (!parentTag.predicates) { - // Can we simply remove this statement? - // activeTag.text = null; + activeTag.text = null; } } From 39bb9090a298064a465fc8d81b1cc8dab52e366d Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Thu, 12 Jun 2025 14:51:15 +0200 Subject: [PATCH 09/13] add failing test regarding older document parsing --- test-app/tests/unit/rdfa/rdfa-parsing-test.ts | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/test-app/tests/unit/rdfa/rdfa-parsing-test.ts b/test-app/tests/unit/rdfa/rdfa-parsing-test.ts index e50909103..6295cef3b 100644 --- a/test-app/tests/unit/rdfa/rdfa-parsing-test.ts +++ b/test-app/tests/unit/rdfa/rdfa-parsing-test.ts @@ -733,4 +733,96 @@ module('rdfa | parsing', function () { const resultingDocument = controller.mainEditorState.doc; assert.propEqual(resultingDocument.toJSON(), initialDocument.toJSON()); }); + test('Older documents with older style literal nodes should be correctly parsed', function (assert) { + QUnit.dump.maxDepth = 10; + const html = ` +
+ +
+
+ +
+

Decision title

+
+
+
+
+ `; + const { doc, block_rdfa, heading } = testBuilders; + const df = new SayDataFactory(); + const expectedDoc = doc( + {}, + block_rdfa( + { + rdfaNodeType: 'resource', + subject: 'http://data.lblod.info/id/besluiten/1', + __rdfaId: '30e2d215-a8b6-4bab-9d8b-aaab634589ea', + properties: [ + { + predicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', + object: df.namedNode( + 'http://data.vlaanderen.be/ns/besluit#Besluit', + ), + }, + { + predicate: 'http://data.europa.eu/eli/ontology#title', + object: df.literalNode('literal-1'), + }, + ] satisfies OutgoingTriple[], + }, + heading( + { + rdfaNodeType: 'literal', + __rdfaId: 'literal-1', + backlinks: [ + { + predicate: 'http://data.europa.eu/eli/ontology#title', + subject: sayDataFactory.resourceNode( + 'http://data.lblod.info/id/besluiten/1', + ), + }, + ], + datatype: sayDataFactory.namedNode( + 'http://www.w3.org/2001/XMLSchema#string', + ), + language: '', + level: 4, + }, + 'Decision title', + ), + ), + ); + const { controller } = testEditor(schema, plugins); + controller.initialize(html); + const actualDoc = controller.mainEditorState.doc; + assert.propEqual(actualDoc.toJSON(), expectedDoc.toJSON()); + }); }); From d3f5676b16dacd56279d4c8ddd032bf1da0a16a0 Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Tue, 3 Jun 2025 14:37:46 +0200 Subject: [PATCH 10/13] fix(rdfa-parser): correctly parse older-style literal nodes --- .changeset/twelve-experts-pick.md | 5 +++++ .../utils/_private/rdfa-parser/post-process-as-rdfa-nodes.ts | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/twelve-experts-pick.md diff --git a/.changeset/twelve-experts-pick.md b/.changeset/twelve-experts-pick.md new file mode 100644 index 000000000..8f0355903 --- /dev/null +++ b/.changeset/twelve-experts-pick.md @@ -0,0 +1,5 @@ +--- +'@lblod/ember-rdfa-editor': patch +--- + +Add extra check to `postProcessTagAsRdfaNode` function to correctly parse older style literal nodes diff --git a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/post-process-as-rdfa-nodes.ts b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/post-process-as-rdfa-nodes.ts index 3edc5893e..1f090e60d 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/post-process-as-rdfa-nodes.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/post-process-as-rdfa-nodes.ts @@ -120,7 +120,9 @@ export function postProcessTagAsRdfaNode(args: PostProcessArgs): void { } else { if ( truthyAttribute(attributes, 'about') && - !truthyAttribute(attributes, 'data-literal-node') + !truthyAttribute(attributes, 'data-literal-node') && + // Is this correct, an alternative solution would be to check on the presence of a `resource` attr here? + !truthyAttribute(attributes, 'datatype') ) { // same exception as above, we always interpret (property +about -content) cases as literal nodes markAsResourceNode( From 4a02d119d35de1d1f3236ceababa834aeebe840e Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Thu, 12 Jun 2025 15:53:29 +0200 Subject: [PATCH 11/13] rdfa postprocessing: add exception for external triple nodes --- .../rdfa-parser/post-process-as-rdfa-nodes.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/post-process-as-rdfa-nodes.ts b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/post-process-as-rdfa-nodes.ts index 1f090e60d..fdfc631b0 100644 --- a/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/post-process-as-rdfa-nodes.ts +++ b/packages/ember-rdfa-editor/src/utils/_private/rdfa-parser/post-process-as-rdfa-nodes.ts @@ -81,6 +81,22 @@ export function postProcessTagAsRdfaNode(args: PostProcessArgs): void { markAsResourceNode, } = args; const node = activeTag.node; + const representsExternalTriple = + node instanceof Node && + node.parentElement?.dataset['externalTripleContainer']; + if (representsExternalTriple) { + markAsResourceNode( + node, + unwrap(activeTag.subject), + activeTag, + activeTag.predicates?.find( + (pred) => pred.value === attributes['property'], + ), + activeTag.datatype, + activeTag.language, + ); + return; + } if (!activeTag.skipElement && node) { // no rel or rev if ( From d4ccdccf847d7b40c7862221e15596f57ae1ede5 Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Fri, 13 Jun 2025 11:43:49 +0200 Subject: [PATCH 12/13] WIP: parse heading as block_rdfa --- .../src/plugins/heading/nodes/heading.ts | 95 ++++++++++--------- test-app/app/controllers/editable-node.ts | 2 +- test-app/tests/unit/rdfa/rdfa-parsing-test.ts | 2 +- 3 files changed, 53 insertions(+), 46 deletions(-) diff --git a/packages/ember-rdfa-editor/src/plugins/heading/nodes/heading.ts b/packages/ember-rdfa-editor/src/plugins/heading/nodes/heading.ts index 93cb34f20..28302df93 100644 --- a/packages/ember-rdfa-editor/src/plugins/heading/nodes/heading.ts +++ b/packages/ember-rdfa-editor/src/plugins/heading/nodes/heading.ts @@ -1,10 +1,5 @@ -import { Node as PNode } from 'prosemirror-model'; -import { - getRdfaAttrs, - getRdfaContentElement, - rdfaAttrSpec, - renderRdfaAware, -} from '#root/core/schema.ts'; +import { Fragment, Node as PNode } from 'prosemirror-model'; +import { getRdfaAttrs, getRdfaContentElement } from '#root/core/schema.ts'; import { optionMapOr } from '#root/utils/_private/option.ts'; import type SayNodeSpec from '#root/core/say-node-spec.ts'; import NumberEditor from '#root/components/_private/utils/number-editor.gts'; @@ -13,14 +8,10 @@ import { DEFAULT_ALIGNMENT, getAlignment } from '../../alignment/index.ts'; import { HEADING_ELEMENTS } from '#root/utils/_private/constants.ts'; import { getHeadingLevel } from '#root/utils/_private/html-utils.ts'; import getClassnamesFromNode from '#root/utils/get-classnames-from-node.ts'; +import type { Schema } from 'prosemirror-model'; +import { ProseParser } from '#root/prosemirror-aliases.ts'; -type Config = { - rdfaAware?: boolean; -}; - -export const headingWithConfig: (config?: Config) => SayNodeSpec = ({ - rdfaAware = false, -} = {}) => { +export const headingWithConfig: () => SayNodeSpec = () => { return { get attrs() { const commonAttrs = { @@ -36,25 +27,54 @@ export const headingWithConfig: (config?: Config) => SayNodeSpec = ({ }, alignment: { default: DEFAULT_ALIGNMENT }, }; - return { ...commonAttrs, ...rdfaAttrSpec({ rdfaAware }) }; + return commonAttrs; }, content: 'inline*', group: 'block', defining: true, - editable: rdfaAware, - isolating: rdfaAware, - selectable: rdfaAware, + editable: false, + isolating: false, + selectable: false, classNames: ['say-heading'], parseDOM: [ + { + tag: HEADING_ELEMENTS.join(','), + node: 'block_rdfa', + getAttrs(node: string | HTMLElement) { + if (!(node instanceof HTMLHeadingElement)) { + return false; + } + const rdfaAttrs = getRdfaAttrs(node, { rdfaAware: true }); + if (!rdfaAttrs) { + return false; + } + return rdfaAttrs; + }, + getContent: (node: HTMLHeadingElement, schema: Schema) => { + const headingAttrs = { + level: getHeadingLevel(node), + indentationLevel: optionMapOr( + 0, + parseInt, + node.dataset['indentationLevel'], + ), + alignment: getAlignment(node), + }; + const parser = ProseParser.fromSchema(schema); + const slice = parser.parseSlice(getRdfaContentElement(node)); + return Fragment.from( + schema.nodes['heading'].create(headingAttrs, slice.content), + ); + }, + }, { tag: HEADING_ELEMENTS.join(','), getAttrs(node: string | HTMLElement) { if (!(node instanceof HTMLHeadingElement)) { return false; } - const level = getHeadingLevel(node); - const baseAttrs = { - level, + const attrs = { + level: getHeadingLevel(node), indentationLevel: optionMapOr( 0, parseInt, @@ -62,9 +82,8 @@ export const headingWithConfig: (config?: Config) => SayNodeSpec = ({ ), alignment: getAlignment(node), }; - return { ...baseAttrs, ...getRdfaAttrs(node, { rdfaAware }) }; + return attrs; }, - contentElement: getRdfaContentElement, }, ], toDOM(node: PNode) { @@ -77,27 +96,15 @@ export const headingWithConfig: (config?: Config) => SayNodeSpec = ({ 'data-indentation-level': indentationLevel as number, style, }; - if (rdfaAware) { - return renderRdfaAware({ - tag: `h${(level as number).toString()}`, - renderable: node, - attrs: { - ...baseAttrs, - class: `say-editable ${getClassnamesFromNode(node)}`, - }, - content: 0, - }); - } else { - return [ - `h${(level as number).toString()}`, - { - ...baseAttrs, - ...node.attrs, - class: getClassnamesFromNode(node), - }, - 0, - ]; - } + return [ + `h${(level as number).toString()}`, + { + ...baseAttrs, + ...node.attrs, + class: getClassnamesFromNode(node), + }, + 0, + ]; }, }; }; diff --git a/test-app/app/controllers/editable-node.ts b/test-app/app/controllers/editable-node.ts index 67e7d65ae..e1ab4cb79 100644 --- a/test-app/app/controllers/editable-node.ts +++ b/test-app/app/controllers/editable-node.ts @@ -221,7 +221,7 @@ export default class EditableBlockController extends Controller { cellContent: 'block+', inlineBorderStyle: { width: '0.5px', color: '#CCD1D9' }, }), - heading: headingWithConfig({ rdfaAware: false }), + heading: headingWithConfig(), blockquote, horizontal_rule, diff --git a/test-app/tests/unit/rdfa/rdfa-parsing-test.ts b/test-app/tests/unit/rdfa/rdfa-parsing-test.ts index 6295cef3b..fb9d4e012 100644 --- a/test-app/tests/unit/rdfa/rdfa-parsing-test.ts +++ b/test-app/tests/unit/rdfa/rdfa-parsing-test.ts @@ -92,7 +92,7 @@ const schema = new Schema({ tableGroup: 'block', cellContent: 'block+', }), - heading: headingWithConfig({ rdfaAware: true }), + heading: headingWithConfig(), blockquote, horizontal_rule, From 978d940bbb9eeafced0028ea8c947e5891608eb9 Mon Sep 17 00:00:00 2001 From: Elena Poelman Date: Fri, 13 Jun 2025 12:01:52 +0200 Subject: [PATCH 13/13] adjust test to take new heading parsing into account --- test-app/tests/unit/rdfa/rdfa-parsing-test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test-app/tests/unit/rdfa/rdfa-parsing-test.ts b/test-app/tests/unit/rdfa/rdfa-parsing-test.ts index fb9d4e012..eb074a972 100644 --- a/test-app/tests/unit/rdfa/rdfa-parsing-test.ts +++ b/test-app/tests/unit/rdfa/rdfa-parsing-test.ts @@ -798,7 +798,7 @@ module('rdfa | parsing', function () { }, ] satisfies OutgoingTriple[], }, - heading( + block_rdfa( { rdfaNodeType: 'literal', __rdfaId: 'literal-1', @@ -814,9 +814,13 @@ module('rdfa | parsing', function () { 'http://www.w3.org/2001/XMLSchema#string', ), language: '', - level: 4, }, - 'Decision title', + heading( + { + level: 4, + }, + 'Decision title', + ), ), ), );