@openuji/speculator 0.7.4 → 0.8.0

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.
Files changed (178) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/parse/assembler.d.ts +1 -1
  7. package/dist/parse/assembler.d.ts.map +1 -1
  8. package/dist/parse/assembler.js +67 -13
  9. package/dist/parse/assembler.js.map +1 -1
  10. package/dist/parse/html/AsideHtmlParser.d.ts.map +1 -1
  11. package/dist/parse/html/AsideHtmlParser.js +85 -3
  12. package/dist/parse/html/AsideHtmlParser.js.map +1 -1
  13. package/dist/parse/html/CodeHtmlParser.d.ts.map +1 -1
  14. package/dist/parse/html/CodeHtmlParser.js +6 -2
  15. package/dist/parse/html/CodeHtmlParser.js.map +1 -1
  16. package/dist/parse/html/HeadingsHtmlParser.d.ts.map +1 -1
  17. package/dist/parse/html/HeadingsHtmlParser.js +3 -4
  18. package/dist/parse/html/HeadingsHtmlParser.js.map +1 -1
  19. package/dist/parse/html/IdlHtmlParser.d.ts +0 -4
  20. package/dist/parse/html/IdlHtmlParser.d.ts.map +1 -1
  21. package/dist/parse/html/IdlHtmlParser.js +2 -138
  22. package/dist/parse/html/IdlHtmlParser.js.map +1 -1
  23. package/dist/parse/html/MiscHtmlParser.d.ts.map +1 -1
  24. package/dist/parse/html/MiscHtmlParser.js +2 -42
  25. package/dist/parse/html/MiscHtmlParser.js.map +1 -1
  26. package/dist/parse/html/SectionsHtmlParser.d.ts.map +1 -1
  27. package/dist/parse/html/SectionsHtmlParser.js +17 -26
  28. package/dist/parse/html/SectionsHtmlParser.js.map +1 -1
  29. package/dist/parse/html/SpecStatementHtmlParser.js +3 -0
  30. package/dist/parse/html/SpecStatementHtmlParser.js.map +1 -1
  31. package/dist/parse/html/VocabHtmlParser.d.ts +12 -0
  32. package/dist/parse/html/VocabHtmlParser.d.ts.map +1 -0
  33. package/dist/parse/html/VocabHtmlParser.js +1156 -0
  34. package/dist/parse/html/VocabHtmlParser.js.map +1 -0
  35. package/dist/parse/html/parser.d.ts +13 -4
  36. package/dist/parse/html/parser.d.ts.map +1 -1
  37. package/dist/parse/html/parser.js +105 -119
  38. package/dist/parse/html/parser.js.map +1 -1
  39. package/dist/parse/index.d.ts +1 -2
  40. package/dist/parse/index.d.ts.map +1 -1
  41. package/dist/parse/index.js +0 -1
  42. package/dist/parse/index.js.map +1 -1
  43. package/dist/parse/markdown/CodeMarkdownParser.d.ts.map +1 -1
  44. package/dist/parse/markdown/CodeMarkdownParser.js +20 -2
  45. package/dist/parse/markdown/CodeMarkdownParser.js.map +1 -1
  46. package/dist/parse/markdown/HeadingsMarkdownParser.d.ts.map +1 -1
  47. package/dist/parse/markdown/HeadingsMarkdownParser.js +10 -33
  48. package/dist/parse/markdown/HeadingsMarkdownParser.js.map +1 -1
  49. package/dist/parse/markdown/MdxJsxMarkdownParser.d.ts +8 -0
  50. package/dist/parse/markdown/MdxJsxMarkdownParser.d.ts.map +1 -0
  51. package/dist/parse/markdown/MdxJsxMarkdownParser.js +61 -0
  52. package/dist/parse/markdown/MdxJsxMarkdownParser.js.map +1 -0
  53. package/dist/parse/markdown/MdxMarkdownParser.d.ts +13 -0
  54. package/dist/parse/markdown/MdxMarkdownParser.d.ts.map +1 -0
  55. package/dist/parse/markdown/MdxMarkdownParser.js +351 -0
  56. package/dist/parse/markdown/MdxMarkdownParser.js.map +1 -0
  57. package/dist/parse/markdown/ShorthandsMarkdownParser.d.ts.map +1 -1
  58. package/dist/parse/markdown/ShorthandsMarkdownParser.js +57 -2
  59. package/dist/parse/markdown/ShorthandsMarkdownParser.js.map +1 -1
  60. package/dist/parse/markdown/parser.d.ts +12 -3
  61. package/dist/parse/markdown/parser.d.ts.map +1 -1
  62. package/dist/parse/markdown/parser.js +90 -57
  63. package/dist/parse/markdown/parser.js.map +1 -1
  64. package/dist/parse/markdown/plugins.d.ts +26 -0
  65. package/dist/parse/markdown/plugins.d.ts.map +1 -0
  66. package/dist/parse/markdown/plugins.js +90 -0
  67. package/dist/parse/markdown/plugins.js.map +1 -0
  68. package/dist/parse/markdown/remark-mdx-jsx-only.d.ts +10 -0
  69. package/dist/parse/markdown/remark-mdx-jsx-only.d.ts.map +1 -0
  70. package/dist/parse/markdown/remark-mdx-jsx-only.js +26 -0
  71. package/dist/parse/markdown/remark-mdx-jsx-only.js.map +1 -0
  72. package/dist/parse/parsers.d.ts +2 -2
  73. package/dist/parse/parsers.d.ts.map +1 -1
  74. package/dist/parse/parsers.js +6 -6
  75. package/dist/parse/parsers.js.map +1 -1
  76. package/dist/parse/pipeline.d.ts.map +1 -1
  77. package/dist/parse/pipeline.js +41 -14
  78. package/dist/parse/pipeline.js.map +1 -1
  79. package/dist/parse/registry.d.ts +5 -5
  80. package/dist/parse/registry.d.ts.map +1 -1
  81. package/dist/parse/registry.js +12 -12
  82. package/dist/parse/registry.js.map +1 -1
  83. package/dist/parse/source-mapper.d.ts +42 -0
  84. package/dist/parse/source-mapper.d.ts.map +1 -0
  85. package/dist/parse/source-mapper.js +117 -0
  86. package/dist/parse/source-mapper.js.map +1 -0
  87. package/dist/parse/types.d.ts +20 -9
  88. package/dist/parse/types.d.ts.map +1 -1
  89. package/dist/parse/types.js +2 -13
  90. package/dist/parse/types.js.map +1 -1
  91. package/dist/parse/utils/hast-utils.d.ts.map +1 -1
  92. package/dist/parse/utils/hast-utils.js +69 -29
  93. package/dist/parse/utils/hast-utils.js.map +1 -1
  94. package/dist/parse/utils/html-element-utils.d.ts +9 -0
  95. package/dist/parse/utils/html-element-utils.d.ts.map +1 -0
  96. package/dist/parse/utils/html-element-utils.js +88 -0
  97. package/dist/parse/utils/html-element-utils.js.map +1 -0
  98. package/dist/parse/utils/idl-tokenizer.d.ts +7 -0
  99. package/dist/parse/utils/idl-tokenizer.d.ts.map +1 -0
  100. package/dist/parse/utils/idl-tokenizer.js +137 -0
  101. package/dist/parse/utils/idl-tokenizer.js.map +1 -0
  102. package/dist/parse/utils/markdown-utils.d.ts +16 -10
  103. package/dist/parse/utils/markdown-utils.d.ts.map +1 -1
  104. package/dist/parse/utils/markdown-utils.js +39 -75
  105. package/dist/parse/utils/markdown-utils.js.map +1 -1
  106. package/dist/pipeline/index.d.ts.map +1 -1
  107. package/dist/pipeline/index.js +1 -0
  108. package/dist/pipeline/index.js.map +1 -1
  109. package/dist/pipeline/runner.d.ts.map +1 -1
  110. package/dist/pipeline/runner.js +10 -4
  111. package/dist/pipeline/runner.js.map +1 -1
  112. package/dist/pipeline/types.d.ts +2 -0
  113. package/dist/pipeline/types.d.ts.map +1 -1
  114. package/dist/postprocess/index.d.ts +3 -1
  115. package/dist/postprocess/index.d.ts.map +1 -1
  116. package/dist/postprocess/index.js +9 -3
  117. package/dist/postprocess/index.js.map +1 -1
  118. package/dist/postprocess/plugins/bibliography-generator.d.ts.map +1 -1
  119. package/dist/postprocess/plugins/bibliography-generator.js +20 -3
  120. package/dist/postprocess/plugins/bibliography-generator.js.map +1 -1
  121. package/dist/postprocess/plugins/citation-resolve.d.ts.map +1 -1
  122. package/dist/postprocess/plugins/citation-resolve.js +255 -11
  123. package/dist/postprocess/plugins/citation-resolve.js.map +1 -1
  124. package/dist/postprocess/plugins/example-index.d.ts +16 -0
  125. package/dist/postprocess/plugins/example-index.d.ts.map +1 -0
  126. package/dist/postprocess/plugins/example-index.js +131 -0
  127. package/dist/postprocess/plugins/example-index.js.map +1 -0
  128. package/dist/postprocess/plugins/note-index.d.ts +14 -0
  129. package/dist/postprocess/plugins/note-index.d.ts.map +1 -0
  130. package/dist/postprocess/plugins/note-index.js +103 -0
  131. package/dist/postprocess/plugins/note-index.js.map +1 -0
  132. package/dist/postprocess/plugins/note-shorthands.d.ts +11 -0
  133. package/dist/postprocess/plugins/note-shorthands.d.ts.map +1 -0
  134. package/dist/postprocess/plugins/note-shorthands.js +298 -0
  135. package/dist/postprocess/plugins/note-shorthands.js.map +1 -0
  136. package/dist/postprocess/plugins/reference-resolve.d.ts.map +1 -1
  137. package/dist/postprocess/plugins/reference-resolve.js +46 -1
  138. package/dist/postprocess/plugins/reference-resolve.js.map +1 -1
  139. package/dist/postprocess/plugins/statement-distribute.d.ts.map +1 -1
  140. package/dist/postprocess/plugins/statement-distribute.js +46 -8
  141. package/dist/postprocess/plugins/statement-distribute.js.map +1 -1
  142. package/dist/postprocess/plugins/toc.d.ts.map +1 -1
  143. package/dist/postprocess/plugins/toc.js +20 -16
  144. package/dist/postprocess/plugins/toc.js.map +1 -1
  145. package/dist/postprocess/walk-ast.d.ts +5 -1
  146. package/dist/postprocess/walk-ast.d.ts.map +1 -1
  147. package/dist/postprocess/walk-ast.js +9 -0
  148. package/dist/postprocess/walk-ast.js.map +1 -1
  149. package/dist/preprocess/config/doc-config.d.ts +4 -1
  150. package/dist/preprocess/config/doc-config.d.ts.map +1 -1
  151. package/dist/preprocess/config/doc-config.js +6 -3
  152. package/dist/preprocess/config/doc-config.js.map +1 -1
  153. package/dist/preprocess/config/normalize.d.ts.map +1 -1
  154. package/dist/preprocess/config/normalize.js +34 -11
  155. package/dist/preprocess/config/normalize.js.map +1 -1
  156. package/dist/preprocess/config/types.d.ts +7 -3
  157. package/dist/preprocess/config/types.d.ts.map +1 -1
  158. package/dist/preprocess/include/resolver.d.ts.map +1 -1
  159. package/dist/preprocess/include/resolver.js +137 -33
  160. package/dist/preprocess/include/resolver.js.map +1 -1
  161. package/dist/preprocess/include/scan-html.js +1 -1
  162. package/dist/preprocess/include/scan-html.js.map +1 -1
  163. package/dist/preprocess/index.d.ts +1 -1
  164. package/dist/preprocess/index.d.ts.map +1 -1
  165. package/dist/preprocess/index.js.map +1 -1
  166. package/dist/preprocess/pipeline.d.ts.map +1 -1
  167. package/dist/preprocess/pipeline.js +1 -1
  168. package/dist/preprocess/pipeline.js.map +1 -1
  169. package/dist/preprocess/types.d.ts +32 -12
  170. package/dist/preprocess/types.d.ts.map +1 -1
  171. package/dist/types/ast.generated.d.ts +69 -13
  172. package/dist/types/ast.generated.d.ts.map +1 -1
  173. package/dist/types/ast.generated.js +4 -4
  174. package/dist/types/ast.generated.js.map +1 -1
  175. package/dist/workspace/sort.js +1 -1
  176. package/dist/workspace/sort.js.map +1 -1
  177. package/package.json +11 -2
  178. package/schema/spec-ast.schema.json +143 -9
@@ -0,0 +1,1156 @@
1
+ /**
2
+ * Vocab HTML Parser
3
+ *
4
+ * Handles <spec-vocab> custom elements.
5
+ * Reads pre-loaded sibling metadata from `unit.sideFiles` (populated at preprocess time)
6
+ * to generate normative prose (Tables, Statements) from vocabulary files.
7
+ *
8
+ * This parser is fully isomorphic — it does NOT access the filesystem directly.
9
+ */
10
+ import * as N3 from 'n3';
11
+ const CONTEXT_FILE_SUFFIX = '.context.jsonld';
12
+ const DEFAULT_CONTEXT_IMPORT_MAX_DEPTH = 20;
13
+ export const VocabHtmlParser = {
14
+ name: 'VocabHtmlParser',
15
+ handles: ['spec-vocab'],
16
+ order: 4,
17
+ handleBlock(element, ctx) {
18
+ const sourcePos = ctx.createSourcePos(element);
19
+ const sideFiles = sourcePos.offset !== undefined ? ctx.sourceMapper.getSideFiles(sourcePos.offset) : undefined;
20
+ if (!sideFiles || Object.keys(sideFiles).length === 0)
21
+ return null;
22
+ const sideFileEntries = toOrderedSideFileEntries(sideFiles);
23
+ const expandedIriAttr = ctx.getAttr(element, 'data-expanded-iri');
24
+ const showExpandedIri = expandedIriAttr === 'data-expanded-iri' || expandedIriAttr === 'true';
25
+ const explicitTarget = getExplicitTargetRequest(element, ctx);
26
+ const contextAttr = ctx.getAttr(element, 'context');
27
+ if (explicitTarget) {
28
+ return resolveTermRequest(explicitTarget, sideFileEntries, ctx, sourcePos);
29
+ }
30
+ if (contextAttr !== undefined) {
31
+ return resolveContextRequest(contextAttr, sideFileEntries, sourcePos.file, sourcePos, sideFiles, showExpandedIri);
32
+ }
33
+ const classFallbackTarget = getClassFallbackTargetRequest(element, ctx);
34
+ if (classFallbackTarget) {
35
+ return resolveTermRequest(classFallbackTarget, sideFileEntries, ctx, sourcePos);
36
+ }
37
+ return null;
38
+ }
39
+ };
40
+ function resolveTermRequest(request, sideFiles, ctx, sourcePos) {
41
+ const term = request.value;
42
+ const ttlFiles = sideFiles.filter((entry) => entry.fileName.endsWith('.ttl'));
43
+ if (ttlFiles.length > 0) {
44
+ const bundle = buildMergedTtlBundle(ttlFiles);
45
+ if (bundle) {
46
+ const termIri = resolveTermIri(term, bundle.prefixes, bundle.store);
47
+ if (termIri) {
48
+ if (request.kind === 'class') {
49
+ return generateClassProse(termIri, bundle.store, ctx, sourcePos);
50
+ }
51
+ return generatePropertyProse(termIri, bundle.store, sourcePos);
52
+ }
53
+ }
54
+ }
55
+ const jsonLdFiles = sideFiles.filter((entry) => entry.fileName.endsWith('.jsonld') && !entry.fileName.endsWith(CONTEXT_FILE_SUFFIX));
56
+ for (const jsonLdFile of jsonLdFiles) {
57
+ const result = parseJsonLd(jsonLdFile.content, request, sourcePos);
58
+ if (result)
59
+ return result;
60
+ }
61
+ return null;
62
+ }
63
+ function resolveContextRequest(contextAttr, sideFiles, unitFile, sourcePos, rawSideFiles, showExpandedIri) {
64
+ const normalizedContextAttr = contextAttr.trim();
65
+ const isDefault = normalizedContextAttr === 'context' || normalizedContextAttr === '';
66
+ const contextFiles = sideFiles.filter((entry) => entry.fileName.endsWith(CONTEXT_FILE_SUFFIX));
67
+ if (contextFiles.length === 0)
68
+ return null;
69
+ if (isDefault) {
70
+ const folderName = getParentFolderName(unitFile);
71
+ if (folderName) {
72
+ const defaultContextFile = `${folderName}${CONTEXT_FILE_SUFFIX}`;
73
+ const matched = contextFiles.find((entry) => entry.fileName === defaultContextFile);
74
+ if (matched) {
75
+ return parseContextJsonLd(matched.content, folderName, sourcePos, rawSideFiles, matched.normalizedPath, showExpandedIri);
76
+ }
77
+ }
78
+ if (contextFiles.length === 1) {
79
+ const onlyContext = contextFiles[0];
80
+ const fallbackName = stripContextFileSuffix(onlyContext.fileName) || folderName || 'default';
81
+ return parseContextJsonLd(onlyContext.content, fallbackName, sourcePos, rawSideFiles, onlyContext.normalizedPath, showExpandedIri);
82
+ }
83
+ return null;
84
+ }
85
+ const contextFileName = normalizedContextAttr.endsWith(CONTEXT_FILE_SUFFIX)
86
+ ? normalizedContextAttr
87
+ : `${normalizedContextAttr}${CONTEXT_FILE_SUFFIX}`;
88
+ const matched = contextFiles.find((entry) => entry.fileName === contextFileName);
89
+ if (!matched)
90
+ return null;
91
+ const contextName = stripContextFileSuffix(matched.fileName) || normalizedContextAttr;
92
+ return parseContextJsonLd(matched.content, contextName, sourcePos, rawSideFiles, matched.normalizedPath, showExpandedIri);
93
+ }
94
+ function getExplicitTargetRequest(element, ctx) {
95
+ const explicitTerm = normalizeAttrValue(ctx.getAttr(element, 'term'));
96
+ if (explicitTerm) {
97
+ return { kind: 'class', value: explicitTerm, source: 'term' };
98
+ }
99
+ const classIri = normalizeAttrValue(ctx.getAttr(element, 'classIri') ?? ctx.getAttr(element, 'class-iri') ?? ctx.getAttr(element, 'classiri'));
100
+ if (classIri) {
101
+ return { kind: 'class', value: classIri, source: 'classIri' };
102
+ }
103
+ const propertyValue = ctx.getAttr(element, 'property');
104
+ const normalizedProperty = normalizeAttrValue(propertyValue);
105
+ if (normalizedProperty) {
106
+ return { kind: 'property', value: normalizedProperty, source: 'property' };
107
+ }
108
+ return null;
109
+ }
110
+ function getClassFallbackTargetRequest(element, ctx) {
111
+ const classValue = ctx.getAttr(element, 'class') ?? ctx.getAttr(element, 'className');
112
+ const classTerm = getFallbackClassTerm(classValue);
113
+ if (!classTerm)
114
+ return null;
115
+ return { kind: 'class', value: classTerm, source: 'class' };
116
+ }
117
+ function normalizeAttrValue(rawValue) {
118
+ if (!rawValue)
119
+ return undefined;
120
+ const normalized = rawValue.trim();
121
+ return normalized.length > 0 ? normalized : undefined;
122
+ }
123
+ function getFallbackClassTerm(rawValue) {
124
+ if (!rawValue)
125
+ return undefined;
126
+ const tokens = rawValue
127
+ .split(/\s+/)
128
+ .map((token) => token.trim())
129
+ .filter((token) => token.length > 0);
130
+ if (tokens.length === 0)
131
+ return undefined;
132
+ const likelyTermToken = tokens.find(isLikelyTermToken);
133
+ if (likelyTermToken)
134
+ return likelyTermToken;
135
+ return tokens.length === 1 ? tokens[0] : undefined;
136
+ }
137
+ function isLikelyTermToken(token) {
138
+ return token.includes(':') || token.startsWith('http://') || token.startsWith('https://');
139
+ }
140
+ function toOrderedSideFileEntries(sideFiles) {
141
+ return Object.entries(sideFiles)
142
+ .map(([path, content]) => ({
143
+ path,
144
+ normalizedPath: normalizePath(path),
145
+ fileName: getFileName(path),
146
+ content,
147
+ }))
148
+ .sort((a, b) => a.normalizedPath.localeCompare(b.normalizedPath));
149
+ }
150
+ function normalizePath(path) {
151
+ const slashNormalized = path.replace(/\\/g, '/').replace(/\/+/g, '/');
152
+ if (slashNormalized.length > 1 && slashNormalized.endsWith('/')) {
153
+ return slashNormalized.slice(0, -1);
154
+ }
155
+ return slashNormalized;
156
+ }
157
+ function getFileName(path) {
158
+ const normalized = normalizePath(path);
159
+ const lastSlash = normalized.lastIndexOf('/');
160
+ if (lastSlash === -1)
161
+ return normalized;
162
+ return normalized.slice(lastSlash + 1);
163
+ }
164
+ function getParentFolderName(path) {
165
+ if (!path)
166
+ return '';
167
+ const normalized = normalizePath(path);
168
+ const lastSlash = normalized.lastIndexOf('/');
169
+ if (lastSlash <= 0)
170
+ return '';
171
+ const parentPath = normalized.slice(0, lastSlash);
172
+ const parentLastSlash = parentPath.lastIndexOf('/');
173
+ if (parentLastSlash === -1)
174
+ return parentPath;
175
+ return parentPath.slice(parentLastSlash + 1);
176
+ }
177
+ function stripContextFileSuffix(fileName) {
178
+ if (fileName.endsWith(CONTEXT_FILE_SUFFIX)) {
179
+ return fileName.slice(0, -CONTEXT_FILE_SUFFIX.length);
180
+ }
181
+ return fileName;
182
+ }
183
+ // ============================================================================
184
+ // TTL Parsing
185
+ // ============================================================================
186
+ function buildMergedTtlBundle(ttlFiles) {
187
+ const store = new N3.Store();
188
+ const prefixes = {};
189
+ let parsedAtLeastOneFile = false;
190
+ for (const ttlFile of ttlFiles) {
191
+ try {
192
+ const parser = new N3.Parser();
193
+ const quads = parser.parse(ttlFile.content, null, (prefix, ns) => {
194
+ if (!prefix || !ns)
195
+ return;
196
+ const namespace = typeof ns === 'string' ? ns : ns.value;
197
+ if (namespace) {
198
+ prefixes[prefix] = namespace;
199
+ }
200
+ });
201
+ store.addQuads(quads);
202
+ parsedAtLeastOneFile = true;
203
+ }
204
+ catch (e) {
205
+ console.warn(`VocabHtmlParser: Failed to parse TTL file ${ttlFile.path}:`, e);
206
+ }
207
+ }
208
+ if (!parsedAtLeastOneFile)
209
+ return null;
210
+ return { store, prefixes };
211
+ }
212
+ function resolveTermIri(term, prefixes, store) {
213
+ const namedNode = N3.DataFactory.namedNode;
214
+ const hasTermInStore = (iri) => (store.countQuads(namedNode(iri), null, null, null) > 0 ||
215
+ store.countQuads(null, null, namedNode(iri), null) > 0);
216
+ // Try prefix expansion first (e.g. "ujg:Node")
217
+ if (term.includes(':')) {
218
+ const colonIdx = term.indexOf(':');
219
+ const prefix = term.slice(0, colonIdx);
220
+ const local = term.slice(colonIdx + 1);
221
+ const ns = prefixes[prefix];
222
+ if (ns) {
223
+ const expanded = ns + local;
224
+ if (hasTermInStore(expanded))
225
+ return expanded;
226
+ }
227
+ }
228
+ // Fallback: match local name in store subjects
229
+ const localName = term.includes(':') ? term.split(':').pop() : term;
230
+ const subject = store.getSubjects(null, null, null).find((s) => s.termType === 'NamedNode' &&
231
+ (s.value.endsWith('#' + localName) || s.value.endsWith('/' + localName)));
232
+ if (subject?.value)
233
+ return subject.value;
234
+ const object = store.getObjects(null, null, null).find((o) => o.termType === 'NamedNode' &&
235
+ (o.value.endsWith('#' + localName) || o.value.endsWith('/' + localName)));
236
+ return object?.termType === 'NamedNode' ? object.value : undefined;
237
+ }
238
+ // ============================================================================
239
+ // JSON-LD Parsing
240
+ // ============================================================================
241
+ function parseJsonLd(content, request, sourcePos) {
242
+ try {
243
+ const parsed = JSON.parse(content);
244
+ const graph = Array.isArray(parsed['@graph'])
245
+ ? parsed['@graph']
246
+ : Array.isArray(parsed)
247
+ ? parsed
248
+ : [parsed];
249
+ const localName = request.value.includes(':') ? request.value.split(':').pop() : request.value;
250
+ const termNode = graph.find((node) => typeof node['@id'] === 'string' &&
251
+ (node['@id'] === request.value ||
252
+ node['@id'].endsWith('#' + localName) ||
253
+ node['@id'].endsWith('/' + localName)));
254
+ if (termNode) {
255
+ if (request.kind === 'class') {
256
+ return generateClassProseFromJson(termNode, sourcePos);
257
+ }
258
+ return generatePropertyProseFromJson(termNode, sourcePos);
259
+ }
260
+ }
261
+ catch (e) {
262
+ console.warn(`VocabHtmlParser: Failed to parse JSON-LD:`, e);
263
+ }
264
+ return null;
265
+ }
266
+ function parseContextJsonLd(content, contextName, sourcePos, sideFiles, contextPath, showExpandedIri = false) {
267
+ try {
268
+ const parsed = JSON.parse(content);
269
+ const doc = parsed;
270
+ const ctxNode = doc['@context'] || doc;
271
+ const flattenedContext = normalizeContext(ctxNode, sideFiles || {}, {
272
+ maxImportDepth: DEFAULT_CONTEXT_IMPORT_MAX_DEPTH,
273
+ contextPath,
274
+ });
275
+ const model = extractContextModel(flattenedContext, contextName);
276
+ return generateContextProse(model, sourcePos, showExpandedIri);
277
+ }
278
+ catch (e) {
279
+ console.warn(`VocabHtmlParser: Failed to parse JSON-LD Context:`, e);
280
+ }
281
+ return null;
282
+ }
283
+ function normalizeContext(ctxNode, sideFiles, options) {
284
+ const flat = {};
285
+ const orderedSideFiles = toOrderedSideFileEntries(sideFiles);
286
+ const visitedImports = new Set();
287
+ const maxImportDepth = options?.maxImportDepth ?? DEFAULT_CONTEXT_IMPORT_MAX_DEPTH;
288
+ const processImport = (importRef, depth, importerPath) => {
289
+ if (depth > maxImportDepth) {
290
+ console.warn(`VocabHtmlParser: Max JSON-LD context import depth (${maxImportDepth}) exceeded for ${importRef}`);
291
+ return;
292
+ }
293
+ const imported = findImportedContext(importRef, orderedSideFiles, importerPath ?? options?.contextPath);
294
+ if (!imported)
295
+ return;
296
+ if (visitedImports.has(imported.id)) {
297
+ return;
298
+ }
299
+ visitedImports.add(imported.id);
300
+ processNode(imported.contextNode, depth + 1, imported.path);
301
+ };
302
+ const processNode = (node, depth, currentContextPath) => {
303
+ if (depth > maxImportDepth) {
304
+ console.warn(`VocabHtmlParser: Max JSON-LD context nesting depth (${maxImportDepth}) exceeded`);
305
+ return;
306
+ }
307
+ if (typeof node === 'string') {
308
+ // It could be an @import string instead of an object
309
+ processImport(node, depth, currentContextPath);
310
+ }
311
+ else if (Array.isArray(node)) {
312
+ for (const item of node) {
313
+ processNode(item, depth, currentContextPath);
314
+ }
315
+ }
316
+ else if (typeof node === 'object' && node !== null) {
317
+ const obj = node;
318
+ // Handle @import
319
+ if (typeof obj['@import'] === 'string') {
320
+ processImport(obj['@import'], depth, currentContextPath);
321
+ }
322
+ // Merge keys (overriding earlier ones, matching JSON-LD behavior)
323
+ for (const [k, v] of Object.entries(obj)) {
324
+ if (k !== '@import') {
325
+ flat[k] = v;
326
+ }
327
+ }
328
+ }
329
+ };
330
+ processNode(ctxNode, 0, options?.contextPath);
331
+ return flat;
332
+ }
333
+ function findImportedContext(reference, sideFiles, importerPath) {
334
+ const sanitizedReference = sanitizeImportReference(reference);
335
+ if (!sanitizedReference)
336
+ return null;
337
+ const candidates = [];
338
+ const seenCandidates = new Set();
339
+ const addCandidate = (candidate) => {
340
+ if (!candidate)
341
+ return;
342
+ if (seenCandidates.has(candidate.normalizedPath))
343
+ return;
344
+ seenCandidates.add(candidate.normalizedPath);
345
+ candidates.push(candidate);
346
+ };
347
+ const normalizedReference = normalizePathSegments(sanitizedReference);
348
+ if (isAbsolutePath(normalizedReference)) {
349
+ addCandidate(sideFiles.find((entry) => entry.normalizedPath === normalizedReference));
350
+ }
351
+ else if (importerPath) {
352
+ const resolved = resolvePathFromImporter(importerPath, normalizedReference);
353
+ addCandidate(sideFiles.find((entry) => entry.normalizedPath === resolved));
354
+ }
355
+ if (normalizedReference.includes('/')) {
356
+ const suffixMatches = sideFiles.filter((entry) => pathEndsWith(entry.normalizedPath, normalizedReference));
357
+ for (const candidate of rankCandidatesByImporter(suffixMatches, importerPath)) {
358
+ addCandidate(candidate);
359
+ }
360
+ }
361
+ const targetFilename = getImportTargetFileName(reference);
362
+ const filenameMatches = sideFiles.filter((entry) => entry.fileName === targetFilename);
363
+ for (const candidate of rankCandidatesByImporter(filenameMatches, importerPath)) {
364
+ addCandidate(candidate);
365
+ }
366
+ for (const candidate of candidates) {
367
+ try {
368
+ const parsed = JSON.parse(candidate.content);
369
+ return {
370
+ id: candidate.normalizedPath,
371
+ path: candidate.normalizedPath,
372
+ contextNode: parsed['@context'] || parsed,
373
+ };
374
+ }
375
+ catch (e) {
376
+ console.warn(`VocabHtmlParser: Failed to parse imported JSON-LD context from ${candidate.path}:`, e);
377
+ }
378
+ }
379
+ return null;
380
+ }
381
+ function getImportTargetFileName(reference) {
382
+ const sanitizedReference = sanitizeImportReference(reference);
383
+ if (!sanitizedReference)
384
+ return '';
385
+ return getFileName(sanitizedReference);
386
+ }
387
+ function sanitizeImportReference(reference) {
388
+ return reference.split('#')[0].split('?')[0].trim();
389
+ }
390
+ function rankCandidatesByImporter(candidates, importerPath) {
391
+ if (candidates.length <= 1 || !importerPath)
392
+ return candidates;
393
+ const importerDir = getDirectoryPath(importerPath);
394
+ return [...candidates].sort((a, b) => {
395
+ const distanceDelta = getPathDistance(importerDir, getDirectoryPath(a.normalizedPath))
396
+ - getPathDistance(importerDir, getDirectoryPath(b.normalizedPath));
397
+ if (distanceDelta !== 0)
398
+ return distanceDelta;
399
+ return a.normalizedPath.localeCompare(b.normalizedPath);
400
+ });
401
+ }
402
+ function getPathDistance(fromPath, toPath) {
403
+ const fromSegments = getPathSegments(fromPath);
404
+ const toSegments = getPathSegments(toPath);
405
+ const commonDepth = getCommonPrefixDepth(fromSegments, toSegments);
406
+ const fromRemaining = fromSegments.length - commonDepth;
407
+ const toRemaining = toSegments.length - commonDepth;
408
+ return fromRemaining + toRemaining;
409
+ }
410
+ function getCommonPrefixDepth(a, b) {
411
+ const limit = Math.min(a.length, b.length);
412
+ let index = 0;
413
+ while (index < limit && a[index] === b[index]) {
414
+ index++;
415
+ }
416
+ return index;
417
+ }
418
+ function getPathSegments(path) {
419
+ const normalized = normalizePathSegments(path);
420
+ const withoutDrive = normalized.replace(/^[A-Za-z]:\//, '');
421
+ const withoutRoot = withoutDrive.startsWith('/') ? withoutDrive.slice(1) : withoutDrive;
422
+ if (!withoutRoot)
423
+ return [];
424
+ return withoutRoot.split('/').filter((segment) => segment.length > 0);
425
+ }
426
+ function getDirectoryPath(path) {
427
+ const normalized = normalizePath(path);
428
+ const lastSlash = normalized.lastIndexOf('/');
429
+ if (lastSlash === -1)
430
+ return '';
431
+ if (lastSlash === 0)
432
+ return '/';
433
+ return normalized.slice(0, lastSlash);
434
+ }
435
+ function isAbsolutePath(path) {
436
+ return path.startsWith('/') || /^[A-Za-z]:\//.test(path);
437
+ }
438
+ function resolvePathFromImporter(importerPath, referencePath) {
439
+ if (isAbsolutePath(referencePath)) {
440
+ return normalizePathSegments(referencePath);
441
+ }
442
+ const importerDir = getDirectoryPath(importerPath);
443
+ const joined = importerDir ? `${importerDir}/${referencePath}` : referencePath;
444
+ return normalizePathSegments(joined);
445
+ }
446
+ function pathEndsWith(candidatePath, referencePath) {
447
+ const normalizedCandidate = normalizePathSegments(candidatePath);
448
+ const normalizedReference = normalizePathSegments(referencePath);
449
+ if (normalizedCandidate === normalizedReference)
450
+ return true;
451
+ return normalizedCandidate.endsWith(`/${normalizedReference}`);
452
+ }
453
+ function normalizePathSegments(path) {
454
+ const normalized = normalizePath(path);
455
+ if (!normalized)
456
+ return normalized;
457
+ const hasDrivePrefix = /^[A-Za-z]:\//.test(normalized);
458
+ const drivePrefix = hasDrivePrefix ? normalized.slice(0, 2) : '';
459
+ const absoluteWithoutDrive = hasDrivePrefix ? normalized.slice(2).startsWith('/') : normalized.startsWith('/');
460
+ const pathWithoutPrefix = hasDrivePrefix ? normalized.slice(2) : normalized;
461
+ const rawSegments = pathWithoutPrefix.split('/');
462
+ const resolved = [];
463
+ for (const segment of rawSegments) {
464
+ if (!segment || segment === '.')
465
+ continue;
466
+ if (segment === '..') {
467
+ if (resolved.length > 0 && resolved[resolved.length - 1] !== '..') {
468
+ resolved.pop();
469
+ }
470
+ else if (!absoluteWithoutDrive) {
471
+ resolved.push('..');
472
+ }
473
+ continue;
474
+ }
475
+ resolved.push(segment);
476
+ }
477
+ const joined = resolved.join('/');
478
+ if (drivePrefix) {
479
+ return joined ? `${drivePrefix}/${joined}` : `${drivePrefix}/`;
480
+ }
481
+ if (absoluteWithoutDrive) {
482
+ return joined ? `/${joined}` : '/';
483
+ }
484
+ return joined;
485
+ }
486
+ function extractContextModel(ctxNode, contextName) {
487
+ const model = {
488
+ name: contextName,
489
+ prefixes: {},
490
+ terms: []
491
+ };
492
+ if (typeof ctxNode['@vocab'] === 'string') {
493
+ model.vocab = ctxNode['@vocab'];
494
+ }
495
+ for (const [key, value] of Object.entries(ctxNode)) {
496
+ if (key.startsWith('@'))
497
+ continue; // Skip keywords like @vocab, @version
498
+ if (typeof value === 'string') {
499
+ if (value === '@nest') { // e.g. "meta": "@nest"
500
+ model.terms.push({ term: key, isNest: true });
501
+ }
502
+ else if (value.startsWith('@')) {
503
+ model.terms.push({ term: key, isAlias: true, targetKeyword: value });
504
+ }
505
+ else {
506
+ const isPrefix = isContextPrefixDeclaration(key, value);
507
+ if (isPrefix) {
508
+ model.prefixes[key] = value;
509
+ }
510
+ model.terms.push({ term: key, iri: value, isPrefix });
511
+ }
512
+ }
513
+ else if (typeof value === 'object' && value !== null) {
514
+ const termDef = value;
515
+ if (typeof termDef['@id'] === 'string') {
516
+ const isPrefix = termDef['@prefix'] === true && isAbsoluteIri(termDef['@id']);
517
+ if (isPrefix) {
518
+ model.prefixes[key] = termDef['@id'];
519
+ }
520
+ const rule = { term: key, iri: termDef['@id'], isPrefix };
521
+ if (typeof termDef['@type'] === 'string') {
522
+ rule.typeCoercion = termDef['@type'];
523
+ }
524
+ if (typeof termDef['@container'] === 'string') {
525
+ rule.container = termDef['@container'];
526
+ }
527
+ model.terms.push(rule);
528
+ }
529
+ }
530
+ }
531
+ return model;
532
+ }
533
+ // ============================================================================
534
+ // Prose Generation
535
+ // ============================================================================
536
+ const RDFS = {
537
+ label: 'http://www.w3.org/2000/01/rdf-schema#label',
538
+ comment: 'http://www.w3.org/2000/01/rdf-schema#comment',
539
+ domain: 'http://www.w3.org/2000/01/rdf-schema#domain',
540
+ range: 'http://www.w3.org/2000/01/rdf-schema#range',
541
+ };
542
+ const SHACL = {
543
+ targetClass: 'http://www.w3.org/ns/shacl#targetClass',
544
+ property: 'http://www.w3.org/ns/shacl#property',
545
+ path: 'http://www.w3.org/ns/shacl#path',
546
+ minCount: 'http://www.w3.org/ns/shacl#minCount',
547
+ maxCount: 'http://www.w3.org/ns/shacl#maxCount',
548
+ datatype: 'http://www.w3.org/ns/shacl#datatype',
549
+ class: 'http://www.w3.org/ns/shacl#class',
550
+ node: 'http://www.w3.org/ns/shacl#node',
551
+ nodeKind: 'http://www.w3.org/ns/shacl#nodeKind',
552
+ };
553
+ function generateClassProse(iri, store, ctx, sourcePos) {
554
+ void ctx; // ctx reserved for future use (e.g. xref linking)
555
+ const blocks = [];
556
+ const comment = getFirstLiteralObjectValue(store, iri, RDFS.comment);
557
+ if (comment) {
558
+ blocks.push({
559
+ type: 'paragraph',
560
+ children: [{ type: 'text', value: comment }],
561
+ sourcePos
562
+ });
563
+ }
564
+ // blocks.push({
565
+ // type: 'paragraph',
566
+ // children: [
567
+ // { type: 'text', value: 'It ' },
568
+ // { type: 'requirement', keyword: 'MUST' },
569
+ // { type: 'text', value: ' satisfy the following schema:' }
570
+ // ],
571
+ // sourcePos
572
+ // });
573
+ const propertyModels = buildClassPropertyModels(iri, store);
574
+ if (propertyModels.length > 0) {
575
+ const rows = [
576
+ {
577
+ type: 'tableRow',
578
+ children: [
579
+ createHeaderCell('Field'),
580
+ createHeaderCell('Requirement'),
581
+ createHeaderCell('Description'),
582
+ createHeaderCell('Value Type')
583
+ ]
584
+ }
585
+ ];
586
+ for (const property of propertyModels) {
587
+ rows.push({
588
+ type: 'tableRow',
589
+ children: [
590
+ createCell(property.name, true),
591
+ createCell(formatRequirement(property.cardinality)),
592
+ createCell(property.comment || property.label || ''),
593
+ createCell(property.valueType, true)
594
+ ]
595
+ });
596
+ }
597
+ blocks.push({
598
+ type: 'table',
599
+ children: rows,
600
+ id: getLocalName(iri),
601
+ sourcePos
602
+ });
603
+ }
604
+ return blocks;
605
+ }
606
+ function generatePropertyProse(propertyIri, store, sourcePos) {
607
+ const blocks = [];
608
+ const propertyName = getLocalName(propertyIri);
609
+ const comment = getFirstLiteralObjectValue(store, propertyIri, RDFS.comment);
610
+ const label = getFirstLiteralObjectValue(store, propertyIri, RDFS.label);
611
+ const domains = getNamedNodeObjectValues(store, propertyIri, RDFS.domain).map((iri) => getLocalName(iri));
612
+ const ranges = getNamedNodeObjectValues(store, propertyIri, RDFS.range).map((iri) => getLocalName(iri));
613
+ const targetClasses = getShaclTargetClassesForProperty(propertyIri, store).map((iri) => getLocalName(iri));
614
+ if (comment) {
615
+ blocks.push({
616
+ type: 'paragraph',
617
+ children: [{ type: 'text', value: comment }],
618
+ sourcePos
619
+ });
620
+ }
621
+ blocks.push({
622
+ type: 'paragraph',
623
+ children: [
624
+ { type: 'text', value: 'The ' },
625
+ { type: 'inlineCode', value: propertyName },
626
+ { type: 'text', value: ' property ' },
627
+ { type: 'requirement', keyword: 'MUST' },
628
+ { type: 'text', value: ' satisfy the following definition metadata:' }
629
+ ],
630
+ sourcePos
631
+ });
632
+ const rows = [
633
+ {
634
+ type: 'tableRow',
635
+ children: [
636
+ createHeaderCell('Constraint'),
637
+ createHeaderCell('Value')
638
+ ]
639
+ },
640
+ {
641
+ type: 'tableRow',
642
+ children: [
643
+ createCell('IRI', true),
644
+ createCell(propertyIri, true)
645
+ ]
646
+ }
647
+ ];
648
+ if (label) {
649
+ rows.push({
650
+ type: 'tableRow',
651
+ children: [
652
+ createCell('Label', true),
653
+ createCell(label)
654
+ ]
655
+ });
656
+ }
657
+ if (domains.length > 0) {
658
+ rows.push({
659
+ type: 'tableRow',
660
+ children: [
661
+ createCell('Domain', true),
662
+ createCell(domains.join(', '), true)
663
+ ]
664
+ });
665
+ }
666
+ if (ranges.length > 0) {
667
+ rows.push({
668
+ type: 'tableRow',
669
+ children: [
670
+ createCell('Range', true),
671
+ createCell(ranges.join(', '), true)
672
+ ]
673
+ });
674
+ }
675
+ if (targetClasses.length > 0) {
676
+ rows.push({
677
+ type: 'tableRow',
678
+ children: [
679
+ createCell('SHACL Target Class', true),
680
+ createCell(targetClasses.join(', '), true)
681
+ ]
682
+ });
683
+ }
684
+ blocks.push({
685
+ type: 'table',
686
+ children: rows,
687
+ id: propertyName,
688
+ sourcePos
689
+ });
690
+ return blocks;
691
+ }
692
+ function buildClassPropertyModels(classIri, store) {
693
+ const propertyNodes = discoverClassProperties(classIri, store);
694
+ return propertyNodes.map((propertyIri) => {
695
+ const shacl = collectShaclConstraintsForClassProperty(classIri, propertyIri, store);
696
+ const rdfsRanges = getNamedNodeObjectValues(store, propertyIri, RDFS.range).map((iri) => getLocalName(iri));
697
+ return {
698
+ iri: propertyIri,
699
+ name: getLocalName(propertyIri),
700
+ label: getFirstLiteralObjectValue(store, propertyIri, RDFS.label),
701
+ comment: getFirstLiteralObjectValue(store, propertyIri, RDFS.comment),
702
+ valueType: resolveValueType(shacl.valueTypes, rdfsRanges),
703
+ cardinality: {
704
+ cardinality: shacl.cardinality,
705
+ contributingShapeCount: shacl.contributingShapeCount,
706
+ hasConflict: hasCardinalityConflict(shacl.cardinality),
707
+ },
708
+ };
709
+ });
710
+ }
711
+ function discoverClassProperties(classIri, store) {
712
+ const propertySet = new Set();
713
+ const namedNode = N3.DataFactory.namedNode;
714
+ const domainQuads = store.getQuads(null, namedNode(RDFS.domain), namedNode(classIri), null);
715
+ for (const quad of domainQuads) {
716
+ if (quad.subject.termType === 'NamedNode') {
717
+ propertySet.add(quad.subject.value);
718
+ }
719
+ }
720
+ const nodeShapes = store.getSubjects(namedNode(SHACL.targetClass), namedNode(classIri), null);
721
+ for (const nodeShape of nodeShapes) {
722
+ const propertyShapes = store.getObjects(nodeShape, namedNode(SHACL.property), null);
723
+ for (const propertyShape of propertyShapes) {
724
+ const pathNode = store.getObjects(propertyShape, namedNode(SHACL.path), null)[0];
725
+ if (pathNode?.termType === 'NamedNode') {
726
+ propertySet.add(pathNode.value);
727
+ }
728
+ }
729
+ }
730
+ return Array.from(propertySet);
731
+ }
732
+ function collectShaclConstraintsForClassProperty(classIri, propertyIri, store) {
733
+ const namedNode = N3.DataFactory.namedNode;
734
+ const nodeShapes = store.getSubjects(namedNode(SHACL.targetClass), namedNode(classIri), null);
735
+ const contributingShapes = new Set();
736
+ let mergedCardinality = null;
737
+ const valueTypes = new Set();
738
+ for (const nodeShape of nodeShapes) {
739
+ const propertyShapes = store.getObjects(nodeShape, namedNode(SHACL.property), null);
740
+ for (const propertyShape of propertyShapes) {
741
+ const pathNode = store.getObjects(propertyShape, namedNode(SHACL.path), null)[0];
742
+ if (!pathNode || pathNode.termType !== 'NamedNode' || pathNode.value !== propertyIri) {
743
+ continue;
744
+ }
745
+ contributingShapes.add(termIdentity(nodeShape));
746
+ const min = parseShaclCountLiteral(store.getObjects(propertyShape, namedNode(SHACL.minCount), null)[0]);
747
+ const max = parseShaclCountLiteral(store.getObjects(propertyShape, namedNode(SHACL.maxCount), null)[0]);
748
+ mergedCardinality = mergeCardinality(mergedCardinality, {
749
+ min: min === null ? undefined : min,
750
+ max: max === null ? undefined : max,
751
+ });
752
+ appendValueTypeTerms(valueTypes, store.getObjects(propertyShape, namedNode(SHACL.datatype), null));
753
+ appendValueTypeTerms(valueTypes, store.getObjects(propertyShape, namedNode(SHACL.class), null));
754
+ appendValueTypeTerms(valueTypes, store.getObjects(propertyShape, namedNode(SHACL.node), null));
755
+ appendValueTypeTerms(valueTypes, store.getObjects(propertyShape, namedNode(SHACL.nodeKind), null));
756
+ }
757
+ }
758
+ return {
759
+ cardinality: mergedCardinality,
760
+ contributingShapeCount: contributingShapes.size,
761
+ valueTypes: Array.from(valueTypes),
762
+ };
763
+ }
764
+ function appendValueTypeTerms(target, terms) {
765
+ for (const term of terms) {
766
+ target.add(termToDisplayText(term));
767
+ }
768
+ }
769
+ function termToDisplayText(term) {
770
+ if (term.termType === 'NamedNode') {
771
+ return getLocalName(term.value);
772
+ }
773
+ if (term.termType === 'Literal') {
774
+ return term.value;
775
+ }
776
+ return term.value;
777
+ }
778
+ function termIdentity(term) {
779
+ if (term.termType === 'BlankNode') {
780
+ return `_:${term.value}`;
781
+ }
782
+ return term.value;
783
+ }
784
+ function resolveValueType(shaclTypes, rdfsRanges) {
785
+ if (shaclTypes.length > 0) {
786
+ return shaclTypes.join(' | ');
787
+ }
788
+ if (rdfsRanges.length > 0) {
789
+ return rdfsRanges.join(' | ');
790
+ }
791
+ return 'unspecified';
792
+ }
793
+ function getShaclTargetClassesForProperty(propertyIri, store) {
794
+ const namedNode = N3.DataFactory.namedNode;
795
+ const results = new Set();
796
+ const nodeShapes = store.getSubjects(null, namedNode(SHACL.property), null);
797
+ for (const nodeShape of nodeShapes) {
798
+ const propertyShapes = store.getObjects(nodeShape, namedNode(SHACL.property), null);
799
+ for (const propertyShape of propertyShapes) {
800
+ const pathNode = store.getObjects(propertyShape, namedNode(SHACL.path), null)[0];
801
+ if (!pathNode || pathNode.termType !== 'NamedNode' || pathNode.value !== propertyIri) {
802
+ continue;
803
+ }
804
+ const targetClasses = store.getObjects(nodeShape, namedNode(SHACL.targetClass), null);
805
+ for (const targetClass of targetClasses) {
806
+ if (targetClass.termType === 'NamedNode') {
807
+ results.add(targetClass.value);
808
+ }
809
+ }
810
+ }
811
+ }
812
+ return Array.from(results);
813
+ }
814
+ function getNamedNodeObjectValues(store, subjectIri, predicateIri) {
815
+ const objects = store.getObjects(N3.DataFactory.namedNode(subjectIri), N3.DataFactory.namedNode(predicateIri), null);
816
+ const values = objects
817
+ .filter((term) => term.termType === 'NamedNode')
818
+ .map((term) => term.value);
819
+ return Array.from(new Set(values));
820
+ }
821
+ function getFirstLiteralObjectValue(store, subjectIri, predicateIri) {
822
+ const object = store.getObjects(N3.DataFactory.namedNode(subjectIri), N3.DataFactory.namedNode(predicateIri), null)[0];
823
+ return object?.termType === 'Literal' ? object.value : undefined;
824
+ }
825
+ function getLocalName(iri) {
826
+ return iri.split(/[#/]/).pop() || iri;
827
+ }
828
+ function mergeCardinality(current, incoming) {
829
+ if (!current)
830
+ return incoming;
831
+ const merged = {};
832
+ const currentMin = current.min ?? 0;
833
+ const incomingMin = incoming.min ?? 0;
834
+ const min = Math.max(currentMin, incomingMin);
835
+ if (min > 0) {
836
+ merged.min = min;
837
+ }
838
+ if (current.max === undefined && incoming.max !== undefined) {
839
+ merged.max = incoming.max;
840
+ }
841
+ else if (current.max !== undefined && incoming.max === undefined) {
842
+ merged.max = current.max;
843
+ }
844
+ else if (current.max !== undefined && incoming.max !== undefined) {
845
+ merged.max = Math.min(current.max, incoming.max);
846
+ }
847
+ return merged;
848
+ }
849
+ function formatRequirement(summary) {
850
+ const { cardinality, hasConflict, contributingShapeCount } = summary;
851
+ if (hasConflict && cardinality) {
852
+ const minValue = cardinality.min ?? 0;
853
+ const maxValue = cardinality.max === undefined ? '*' : String(cardinality.max);
854
+ return `conflicting constraints (${minValue}..${maxValue})`;
855
+ }
856
+ if (!cardinality || (cardinality.min === undefined && cardinality.max === undefined)) {
857
+ return 'optional (unspecified)';
858
+ }
859
+ const min = cardinality.min ?? 0;
860
+ const max = cardinality.max;
861
+ let base = 'optional (0..*)';
862
+ if (min >= 1 && max === 1)
863
+ base = 'required (1..1)';
864
+ else if (min >= 1 && max !== undefined)
865
+ base = `required (${min}..${max})`;
866
+ else if (min >= 1)
867
+ base = `required (${min}..*)`;
868
+ else if (max === 1)
869
+ base = 'optional (0..1)';
870
+ else if (max !== undefined)
871
+ base = `optional (0..${max})`;
872
+ if (contributingShapeCount > 1 && base !== 'optional (unspecified)') {
873
+ return `${base} [effective across ${contributingShapeCount} shapes]`;
874
+ }
875
+ return base;
876
+ }
877
+ function parseShaclCountLiteral(term) {
878
+ if (!term)
879
+ return null;
880
+ if (term.termType !== 'Literal')
881
+ return null;
882
+ const parsed = Number(term.value);
883
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) {
884
+ return null;
885
+ }
886
+ return parsed;
887
+ }
888
+ function hasCardinalityConflict(cardinality) {
889
+ if (!cardinality)
890
+ return false;
891
+ if (cardinality.min === undefined || cardinality.max === undefined)
892
+ return false;
893
+ return cardinality.min > cardinality.max;
894
+ }
895
+ function generateClassProseFromJson(node, sourcePos) {
896
+ const blocks = [];
897
+ const comment = node['rdfs:comment'] || node['comment'] || node['description'];
898
+ if (comment) {
899
+ const commentText = typeof comment === 'string'
900
+ ? comment
901
+ : comment['@value'] || '';
902
+ blocks.push({
903
+ type: 'paragraph',
904
+ children: [{ type: 'text', value: commentText }],
905
+ sourcePos
906
+ });
907
+ }
908
+ blocks.push({
909
+ type: 'paragraph',
910
+ children: [
911
+ {
912
+ type: 'text',
913
+ value: 'Informative fallback: JSON-LD term metadata was found, but normative schema extraction requires Turtle/SHACL side files.'
914
+ }
915
+ ],
916
+ sourcePos
917
+ });
918
+ return blocks;
919
+ }
920
+ function generatePropertyProseFromJson(node, sourcePos) {
921
+ const blocks = [];
922
+ const comment = node['rdfs:comment'] || node['comment'] || node['description'];
923
+ if (comment) {
924
+ const commentText = typeof comment === 'string'
925
+ ? comment
926
+ : comment['@value'] || '';
927
+ blocks.push({
928
+ type: 'paragraph',
929
+ children: [{ type: 'text', value: commentText }],
930
+ sourcePos
931
+ });
932
+ }
933
+ blocks.push({
934
+ type: 'paragraph',
935
+ children: [
936
+ {
937
+ type: 'text',
938
+ value: 'Informative fallback: JSON-LD property metadata was found, but normative schema extraction requires Turtle/SHACL side files.'
939
+ }
940
+ ],
941
+ sourcePos
942
+ });
943
+ return blocks;
944
+ }
945
+ function generateContextProse(model, sourcePos, showExpandedIri = false) {
946
+ const blocks = [];
947
+ // @vocab
948
+ if (model.vocab) {
949
+ blocks.push({
950
+ type: 'paragraph',
951
+ children: [
952
+ { type: 'text', value: 'The ' },
953
+ { type: 'inlineCode', value: model.name },
954
+ { type: 'text', value: ' JSON-LD context ' },
955
+ { type: 'requirement', keyword: 'MUST' },
956
+ { type: 'text', value: ' set ' },
957
+ { type: 'inlineCode', value: '@vocab' },
958
+ { type: 'text', value: ' to the ' },
959
+ { type: 'inlineCode', value: model.vocab },
960
+ { type: 'text', value: ' namespace.' }
961
+ ],
962
+ sourcePos
963
+ });
964
+ }
965
+ // Term mappings
966
+ for (const rule of model.terms) {
967
+ if (rule.isNest) {
968
+ blocks.push({
969
+ type: 'paragraph',
970
+ children: [
971
+ { type: 'text', value: 'The ' },
972
+ { type: 'inlineCode', value: rule.term },
973
+ { type: 'text', value: ' term is an ' },
974
+ { type: 'inlineCode', value: '@nest' },
975
+ { type: 'text', value: ' alias; nested members ' },
976
+ { type: 'requirement', keyword: 'MUST' },
977
+ { type: 'text', value: ' be interpreted as direct properties.' }
978
+ ],
979
+ sourcePos
980
+ });
981
+ }
982
+ else if (rule.isAlias) {
983
+ blocks.push({
984
+ type: 'paragraph',
985
+ children: [
986
+ { type: 'text', value: 'The ' },
987
+ { type: 'inlineCode', value: rule.term },
988
+ { type: 'text', value: ' term ' },
989
+ { type: 'requirement', keyword: 'MUST' },
990
+ { type: 'text', value: ' be an alias for the JSON-LD ' },
991
+ { type: 'inlineCode', value: rule.targetKeyword },
992
+ { type: 'text', value: ' keyword.' }
993
+ ],
994
+ sourcePos
995
+ });
996
+ }
997
+ else if (rule.iri) {
998
+ const expandedIri = showExpandedIri ? expandContextIri(rule.iri, model) : rule.iri;
999
+ if (rule.isPrefix) {
1000
+ blocks.push({
1001
+ type: 'paragraph',
1002
+ children: [
1003
+ { type: 'text', value: 'The ' },
1004
+ { type: 'inlineCode', value: rule.term },
1005
+ { type: 'text', value: ' prefix ' },
1006
+ { type: 'requirement', keyword: 'MUST' },
1007
+ { type: 'text', value: ' expand to ' },
1008
+ { type: 'inlineCode', value: expandedIri },
1009
+ { type: 'text', value: '.' }
1010
+ ],
1011
+ sourcePos
1012
+ });
1013
+ continue;
1014
+ }
1015
+ if (rule.typeCoercion === '@id') {
1016
+ blocks.push({
1017
+ type: 'paragraph',
1018
+ children: [
1019
+ { type: 'text', value: 'The ' },
1020
+ { type: 'inlineCode', value: rule.term },
1021
+ { type: 'text', value: ' term maps to ' },
1022
+ { type: 'inlineCode', value: expandedIri },
1023
+ { type: 'text', value: ' and values ' },
1024
+ { type: 'requirement', keyword: 'MUST' },
1025
+ { type: 'text', value: ' be interpreted as IRIs.' }
1026
+ ],
1027
+ sourcePos
1028
+ });
1029
+ }
1030
+ else if (rule.typeCoercion === '@json') {
1031
+ blocks.push({
1032
+ type: 'paragraph',
1033
+ children: [
1034
+ { type: 'text', value: 'The ' },
1035
+ { type: 'inlineCode', value: rule.term },
1036
+ { type: 'text', value: ' term ' },
1037
+ { type: 'requirement', keyword: 'MAY' },
1038
+ { type: 'text', value: ' be represented as an ' },
1039
+ { type: 'inlineCode', value: '@json' },
1040
+ { type: 'text', value: ' literal.' }
1041
+ ],
1042
+ sourcePos
1043
+ });
1044
+ }
1045
+ else if (rule.typeCoercion) {
1046
+ blocks.push({
1047
+ type: 'paragraph',
1048
+ children: [
1049
+ { type: 'text', value: 'The ' },
1050
+ { type: 'inlineCode', value: rule.term },
1051
+ { type: 'text', value: ' term maps to ' },
1052
+ { type: 'inlineCode', value: expandedIri },
1053
+ { type: 'text', value: ' and values ' },
1054
+ { type: 'requirement', keyword: 'MUST' },
1055
+ { type: 'text', value: ' be of type ' },
1056
+ { type: 'inlineCode', value: expandContextType(rule.typeCoercion, model) },
1057
+ { type: 'text', value: '.' }
1058
+ ],
1059
+ sourcePos
1060
+ });
1061
+ }
1062
+ else {
1063
+ blocks.push({
1064
+ type: 'paragraph',
1065
+ children: [
1066
+ { type: 'text', value: 'The ' },
1067
+ { type: 'inlineCode', value: rule.term },
1068
+ { type: 'text', value: ' term ' },
1069
+ { type: 'requirement', keyword: 'MUST' },
1070
+ { type: 'text', value: ' map to ' },
1071
+ { type: 'inlineCode', value: expandedIri },
1072
+ { type: 'text', value: '.' }
1073
+ ],
1074
+ sourcePos
1075
+ });
1076
+ }
1077
+ if (rule.container === '@set') {
1078
+ blocks.push({
1079
+ type: 'paragraph',
1080
+ children: [
1081
+ { type: 'text', value: 'The ' },
1082
+ { type: 'inlineCode', value: rule.term },
1083
+ { type: 'text', value: ' term uses ' },
1084
+ { type: 'inlineCode', value: '@set' },
1085
+ { type: 'text', value: '; processors ' },
1086
+ { type: 'requirement', keyword: 'MUST' },
1087
+ { type: 'text', value: ' accept array form consistently for single or multiple values.' }
1088
+ ],
1089
+ sourcePos
1090
+ });
1091
+ }
1092
+ }
1093
+ }
1094
+ return blocks;
1095
+ }
1096
+ function isContextPrefixDeclaration(term, iri) {
1097
+ if (term.includes(':'))
1098
+ return false;
1099
+ if (!isAbsoluteIri(iri))
1100
+ return false;
1101
+ return iri.endsWith('/') || iri.endsWith('#');
1102
+ }
1103
+ function isAbsoluteIri(value) {
1104
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
1105
+ }
1106
+ function expandContextIri(value, model) {
1107
+ if (!value || value.startsWith('@'))
1108
+ return value;
1109
+ if (isAbsoluteIri(value)) {
1110
+ const colonIndex = value.indexOf(':');
1111
+ const prefix = colonIndex === -1 ? value : value.slice(0, colonIndex);
1112
+ if (colonIndex !== -1 && model.prefixes[prefix]) {
1113
+ return model.prefixes[prefix] + value.slice(colonIndex + 1);
1114
+ }
1115
+ return value;
1116
+ }
1117
+ if (value.includes(':')) {
1118
+ const colonIndex = value.indexOf(':');
1119
+ const prefix = value.slice(0, colonIndex);
1120
+ const local = value.slice(colonIndex + 1);
1121
+ const ns = model.prefixes[prefix];
1122
+ if (ns)
1123
+ return ns + local;
1124
+ return value;
1125
+ }
1126
+ if (model.vocab) {
1127
+ return `${model.vocab}${value}`;
1128
+ }
1129
+ return value;
1130
+ }
1131
+ function expandContextType(value, model) {
1132
+ if (value.startsWith('@'))
1133
+ return value;
1134
+ return expandContextIri(value, model);
1135
+ }
1136
+ // ============================================================================
1137
+ // Cell helpers
1138
+ // ============================================================================
1139
+ function createHeaderCell(text) {
1140
+ return {
1141
+ type: 'tableCell',
1142
+ header: true,
1143
+ children: [{ type: 'text', value: text }]
1144
+ };
1145
+ }
1146
+ function createCell(text, code = false) {
1147
+ return {
1148
+ type: 'tableCell',
1149
+ children: [
1150
+ code
1151
+ ? { type: 'inlineCode', value: text }
1152
+ : { type: 'text', value: text }
1153
+ ]
1154
+ };
1155
+ }
1156
+ //# sourceMappingURL=VocabHtmlParser.js.map