mdld-parse 0.7.6 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -198,13 +198,14 @@ Parse MD-LD markdown and return RDF quads with lean origin tracking.
198
198
  - `context` (object) — Prefix mappings (default: `{ '@vocab': 'http://www.w3.org/2000/01/rdf-schema#', rdf, rdfs, xsd, sh, prov }`)
199
199
  - `dataFactory` (object) — Custom RDF/JS DataFactory
200
200
 
201
- **Returns:** `{ quads, remove, statements, origin, context }`
201
+ **Returns:** `{ quads, remove, statements, origin, context, primarySubject }`
202
202
 
203
203
  - `quads` — Array of RDF/JS Quads (final resolved graph state)
204
204
  - `remove` — Array of RDF/JS Quads (external retractions targeting prior state)
205
205
  - `statements` — Array of elevated RDF/JS Quads extracted from rdf:Statement patterns
206
206
  - `origin` — Lean origin tracking object with quadIndex for UI navigation
207
207
  - `context` — Final context used (includes prefixes)
208
+ - `primarySubject` — String IRI or null (first non-fragment subject declaration)
208
209
 
209
210
  ### `merge(docs, options)`
210
211
 
@@ -215,15 +216,22 @@ Merge multiple MDLD documents with diff polarity resolution.
215
216
  - `options` (object, optional):
216
217
  - `context` (object) — Prefix mappings (merged with DEFAULT_CONTEXT)
217
218
 
218
- **Returns:** `{ quads, remove, origin, context }`
219
+ **Returns:** `{ quads, remove, origin, context, primarySubjects }`
219
220
 
220
- ### `generate(quads, context)`
221
+ - `quads` — Array of RDF/JS Quads (final resolved graph state)
222
+ - `remove` — Array of RDF/JS Quads (external retractions targeting prior state)
223
+ - `origin` — Merge origin tracking with document index and polarity
224
+ - `context` — Final merged context
225
+ - `primarySubjects` — Array of string IRIs (primary subjects from each document, in merge order)
226
+
227
+ ### `generate(quads, context, primarySubject)`
221
228
 
222
229
  Generate deterministic MDLD from RDF quads.
223
230
 
224
231
  **Parameters:**
225
232
  - `quads` (array) — Array of RDF/JS Quads to convert
226
233
  - `context` (object, optional) — Prefix mappings (default: `{}`)
234
+ - `primarySubject` (string, optional) — String IRI to place first in output (ensures round-trip safety)
227
235
 
228
236
  **Returns:** `{ text, context }`
229
237
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdld-parse",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "A standards-compliant parser for **MD-LD (Markdown-Linked Data)** — a human-friendly RDF authoring format that extends Markdown with semantic annotations.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/src/generate.js CHANGED
@@ -34,17 +34,17 @@ export function extractLocalName(iri, ctx = {}) {
34
34
  /**
35
35
  * Generate deterministic MDLD from RDF quads
36
36
  * Purpose: TTL→MDLD conversion with canonical structure
37
- * Input: RDF quads + context
37
+ * Input: RDF quads + context + optional primarySubject (string IRI)
38
38
  * Output: MDLD text
39
39
  */
40
- export function generate(quads, context = {}) {
40
+ export function generate(quads, context = {}, primarySubject = null) {
41
41
  const fullContext = { ...DEFAULT_CONTEXT, ...context };
42
42
 
43
43
  const normalizedQuads = normalizeAndSortQuads(quads);
44
44
 
45
45
  const subjectGroups = groupQuadsBySubject(normalizedQuads);
46
46
 
47
- const { text } = buildDeterministicMDLD(subjectGroups, fullContext);
47
+ const { text } = buildDeterministicMDLD(subjectGroups, fullContext, primarySubject);
48
48
 
49
49
  return text;
50
50
  }
@@ -86,7 +86,7 @@ function groupQuadsBySubject(quads) {
86
86
  return groups;
87
87
  }
88
88
 
89
- function buildDeterministicMDLD(subjectGroups, context) {
89
+ function buildDeterministicMDLD(subjectGroups, context, primarySubject = null) {
90
90
  let text = '';
91
91
  const usedPrefixes = collectUsedPrefixes(subjectGroups, context);
92
92
 
@@ -103,10 +103,16 @@ function buildDeterministicMDLD(subjectGroups, context) {
103
103
  text += '\n';
104
104
  }
105
105
 
106
- // Process subjects in deterministic order
106
+ // Process subjects in deterministic order, with primary subject first
107
107
  const sortedSubjects = Array.from(subjectGroups.keys()).sort();
108
+ const primarySubjectIRI = primarySubject; // Already a string IRI
108
109
 
109
- for (const subjectIRI of sortedSubjects) {
110
+ // If primary subject exists, place it first
111
+ const orderedSubjects = primarySubjectIRI
112
+ ? [primarySubjectIRI, ...sortedSubjects.filter(s => s !== primarySubjectIRI)]
113
+ : sortedSubjects;
114
+
115
+ for (const subjectIRI of orderedSubjects) {
110
116
  const subjectQuads = subjectGroups.get(subjectIRI);
111
117
  const shortSubject = shortenIRI(subjectIRI, context);
112
118
 
@@ -119,7 +125,7 @@ function buildDeterministicMDLD(subjectGroups, context) {
119
125
  ? ' ' + types.map(t => '.' + shortenIRI(t.object.value, context)).sort().join(' ')
120
126
  : '';
121
127
 
122
- text += `# ${localSubjectName} {=${shortSubject}${typeAnnotations}}\n`;
128
+ text += `# ${localSubjectName} {=${shortSubject}${typeAnnotations}}\n\n`;
123
129
 
124
130
  // Add literals and objects using shared utilities
125
131
  sortQuadsByPredicate(literals).forEach(quad => {
package/src/merge.js CHANGED
@@ -42,6 +42,7 @@ export function merge(docs, options = {}) {
42
42
  const quadIndex = new Map();
43
43
  const allStatements = []; // Collect statements from all documents
44
44
  const accumulatedContext = new Map(); // Track all unique prefixes across documents
45
+ const primarySubjects = []; // Collect primary subjects from all documents
45
46
 
46
47
  // Process each document in order
47
48
  for (let i = 0; i < docs.length; i++) {
@@ -78,6 +79,11 @@ export function merge(docs, options = {}) {
78
79
  allStatements.push(...doc.statements);
79
80
  }
80
81
 
82
+ // Collect primary subject from this document (already a string IRI)
83
+ if (doc.primarySubject) {
84
+ primarySubjects.push(doc.primarySubject);
85
+ }
86
+
81
87
  // Fold assertions into session buffer
82
88
  for (const quad of doc.quads) {
83
89
  const key = quadKey(quad);
@@ -144,6 +150,7 @@ export function merge(docs, options = {}) {
144
150
  remove: filteredRemove,
145
151
  statements: allStatements, // Include all collected statements
146
152
  origin: mergeOrigin,
147
- context: finalContext
153
+ context: finalContext,
154
+ primarySubjects: primarySubjects // Include all collected primary subjects
148
155
  };
149
156
  }
package/src/parse.js CHANGED
@@ -49,6 +49,7 @@ export function parse(text, options = {}) {
49
49
  const state = {
50
50
  ctx: { ...DEFAULT_CONTEXT, ...(options.context || {}) },
51
51
  df: options.dataFactory || DataFactory,
52
+ graph: DataFactory.namedNode(options.graph) || DataFactory.defaultGraph(),
52
53
  quads: [],
53
54
  quadBuffer: new Map(),
54
55
  removeSet: new Set(),
@@ -58,6 +59,7 @@ export function parse(text, options = {}) {
58
59
  documentStructure: []
59
60
  },
60
61
  currentSubject: null,
62
+ primarySubject: null,
61
63
  tokens: null,
62
64
  currentTokenIndex: -1,
63
65
  statements: [],
@@ -107,7 +109,7 @@ export function parse(text, options = {}) {
107
109
  }
108
110
  }
109
111
 
110
- return { quads: state.quads, remove: filteredRemove, statements: state.statements, origin: state.origin, context: state.ctx };
112
+ return { quads: state.quads, remove: filteredRemove, statements: state.statements, origin: state.origin, context: state.ctx, primarySubject: state.primarySubject };
111
113
  }
112
114
 
113
115
 
@@ -387,6 +389,11 @@ function processAnnotationWithBlockTracking(carrier, sem, state, options = {}) {
387
389
  const newSubject = resolveSubject(sem, state);
388
390
  const localObject = resolveObject(sem, state);
389
391
 
392
+ // Track primary subject: first non-fragment subject declaration (fixed once detected)
393
+ if (newSubject && !state.primarySubject && !sem.subject.startsWith('=#')) {
394
+ state.primarySubject = newSubject.value; // Store as string IRI
395
+ }
396
+
390
397
  const effectiveSubject = implicitSubject || (newSubject && !preserveGlobalSubject ? newSubject : previousSubject);
391
398
  if (newSubject && !preserveGlobalSubject && !implicitSubject) {
392
399
  state.currentSubject = newSubject;