Skip to content

Commit c4e9f76

Browse files
Add @view support - self-describing JSON-LD views
Implements the @view proposal (W3C issue #384): - parseJsonLdToStore(): parse JSON-LD data island into RDF store - renderView(): load pane from @view URL and render Usage: <script type="application/ld+json"> { "@view": "https://example.com/pane.js", ... } </script> <script src="solid-shim.js"></script> <script>solidShim.renderView()</script> See: w3c/json-ld-syntax#384
1 parent 987907c commit c4e9f76

File tree

1 file changed

+171
-1
lines changed

1 file changed

+171
-1
lines changed

src/index.ts

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,178 @@
33
*
44
* Drop-in replacement for mashlib using solid-panes-jss with solid-oidc authentication.
55
* Provides the same interface as mashlib for easy testing and migration.
6+
*
7+
* Supports @view proposal for JSON-LD (self-describing view hint)
8+
* See: https://github.com/w3c/json-ld-syntax/issues/384
69
*/
710

811
import * as $rdf from 'rdflib'
912
import * as panes from 'solid-panes-jss'
1013
import * as UI from 'solid-ui-jss'
1114
import { authn, solidLogicSingleton, authSession, store } from 'solid-logic-jss'
1215
import { versionInfo } from './versionInfo'
13-
import { shimStyle } from './styles'
16+
import { shimStyle, injectJssTheme } from './styles'
1417

1518
const global: any = window
19+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'
20+
21+
/**
22+
* Pane module interface for @view
23+
*/
24+
interface PaneModule {
25+
name?: string
26+
render: (subject: $rdf.NamedNode, context: PaneContext) => HTMLElement
27+
}
28+
29+
interface PaneContext {
30+
dom: Document
31+
session: { store: $rdf.Store }
32+
}
33+
34+
/**
35+
* Parse JSON-LD data island from page into RDF store
36+
*/
37+
function parseJsonLdToStore(jsonld: any, baseUri: string, targetStore: $rdf.Store): $rdf.NamedNode {
38+
const rootId = baseUri + (jsonld['@id'] || '#thing')
39+
40+
function addTriples(node: any, subjectUri: string) {
41+
const subject = $rdf.sym(subjectUri)
42+
43+
// Add type
44+
if (node['@type']) {
45+
const typeUri = node['@type'].replace('schema:', 'http://schema.org/')
46+
targetStore.add(subject, $rdf.sym(RDF_TYPE), $rdf.sym(typeUri))
47+
}
48+
49+
// Add properties
50+
Object.entries(node).forEach(([key, val]: [string, any]) => {
51+
if (key.startsWith('@')) return
52+
const pred = $rdf.sym(key.replace('schema:', 'http://schema.org/'))
53+
54+
if (Array.isArray(val)) {
55+
val.forEach((item: any, i: number) => {
56+
if (typeof item === 'object' && !item['@id']) {
57+
const blankId = `${subjectUri}_${key}_${i}`
58+
targetStore.add(subject, pred, $rdf.sym(blankId))
59+
addTriples(item, blankId)
60+
} else if (typeof item === 'object' && item['@id']) {
61+
const uri = item['@id'].startsWith('http') ? item['@id'] : baseUri + item['@id']
62+
targetStore.add(subject, pred, $rdf.sym(uri))
63+
} else {
64+
targetStore.add(subject, pred, item)
65+
}
66+
})
67+
} else if (typeof val === 'object' && val['@id']) {
68+
const uri = val['@id'].startsWith('http') ? val['@id'] : baseUri + val['@id']
69+
targetStore.add(subject, pred, $rdf.sym(uri))
70+
} else if (typeof val === 'object') {
71+
const blankId = `${subjectUri}_${key}`
72+
targetStore.add(subject, pred, $rdf.sym(blankId))
73+
addTriples(val, blankId)
74+
} else {
75+
targetStore.add(subject, pred, val)
76+
}
77+
})
78+
}
79+
80+
addTriples(jsonld, rootId)
81+
return $rdf.sym(rootId)
82+
}
83+
84+
/**
85+
* Render JSON-LD using @view
86+
*
87+
* Implements the @view proposal: data specifies its own preferred renderer.
88+
* See: https://github.com/w3c/json-ld-syntax/issues/384
89+
*
90+
* @param options.target - Element to render into (default: creates one after JSON-LD script)
91+
* @param options.subject - Subject ID to render (default: @id from JSON-LD or '#thing')
92+
* @param options.fallbackPane - Pane to use if @view fails or is missing
93+
*/
94+
async function renderView(options: {
95+
target?: HTMLElement | string
96+
subject?: string
97+
fallbackPane?: PaneModule
98+
} = {}): Promise<{ subject: $rdf.NamedNode; store: $rdf.Store } | null> {
99+
// Find JSON-LD on page
100+
const jsonLdScript = document.querySelector('script[type="application/ld+json"]')
101+
if (!jsonLdScript) {
102+
console.warn('[solid-shim] No JSON-LD found on page')
103+
return null
104+
}
105+
106+
let jsonld: any
107+
try {
108+
jsonld = JSON.parse(jsonLdScript.textContent || '')
109+
} catch (e) {
110+
console.error('[solid-shim] Invalid JSON-LD:', e)
111+
return null
112+
}
113+
114+
// Parse into store
115+
const baseUri = window.location.href.split('#')[0]
116+
const subjectId = options.subject || jsonld['@id'] || '#thing'
117+
const subject = parseJsonLdToStore(jsonld, baseUri, store)
118+
119+
// Override subject if specified
120+
const finalSubject = options.subject
121+
? $rdf.sym(baseUri + options.subject)
122+
: subject
123+
124+
// Load pane from @view or fallback
125+
let pane: PaneModule | undefined
126+
if (jsonld['@view']) {
127+
try {
128+
console.log(`[solid-shim] Loading @view: ${jsonld['@view']}`)
129+
const paneModule = await import(/* webpackIgnore: true */ jsonld['@view'])
130+
pane = paneModule.default || paneModule
131+
} catch (err) {
132+
console.warn(`[solid-shim] Failed to load @view "${jsonld['@view']}":`, err)
133+
}
134+
}
135+
136+
if (!pane && options.fallbackPane) {
137+
pane = options.fallbackPane
138+
}
139+
140+
if (!pane) {
141+
console.warn('[solid-shim] No @view specified and no fallback pane provided')
142+
return { subject: finalSubject, store }
143+
}
144+
145+
// Find or create target element
146+
let target: HTMLElement | null = null
147+
if (typeof options.target === 'string') {
148+
target = document.querySelector(options.target)
149+
} else if (options.target) {
150+
target = options.target
151+
}
152+
153+
if (!target) {
154+
target = document.createElement('div')
155+
target.id = 'solid-shim-view'
156+
jsonLdScript.parentNode?.insertBefore(target, jsonLdScript.nextSibling)
157+
}
158+
159+
// Render
160+
const context: PaneContext = {
161+
dom: document,
162+
session: { store }
163+
}
164+
165+
try {
166+
const rendered = pane.render(finalSubject, context)
167+
target.appendChild(rendered)
168+
} catch (e: any) {
169+
console.error('[solid-shim] Render error:', e)
170+
target.innerHTML = `<p style="color: #dc2626; padding: 1rem;">Error rendering: ${e.message}</p>`
171+
}
172+
173+
return { subject: finalSubject, store }
174+
}
175+
176+
// Inject JSS theme styles immediately
177+
injectJssTheme()
16178

17179
// Expose globals - same interface as mashlib
18180
global.$rdf = $rdf
@@ -28,6 +190,12 @@ global.mashlib = {
28190
versionInfo
29191
}
30192

193+
// Expose @view renderer globally
194+
global.solidShim = {
195+
renderView,
196+
parseJsonLdToStore
197+
}
198+
31199
/**
32200
* Initialize and run the Solid data browser
33201
* @param uri - Optional URI to display initially
@@ -81,3 +249,5 @@ global.dump = dump
81249
// Export for ES modules
82250
export { versionInfo }
83251
export { $rdf, panes, UI, authn, authSession, store, solidLogicSingleton }
252+
export { renderView, parseJsonLdToStore }
253+
export type { PaneModule, PaneContext }

0 commit comments

Comments
 (0)