-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathdataContentPane.ts
More file actions
313 lines (287 loc) · 12 KB
/
Copy pathdataContentPane.ts
File metadata and controls
313 lines (287 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
/* Data content Pane
**
** This pane shows the content of a particular RDF resource
** or at least the RDF semantics we attribute to that resource.
*/
// To do: - Only take data from one graph
// - Only do forwards not backward?
// - Expand automatically all the way down
// - original source view? Use ffox view source
import * as UI from 'solid-ui'
import * as $rdf from 'rdflib'
import DOMPurify from 'dompurify'
import type { DataBrowserContext, RenderEnvironment } from 'pane-registry'
import type {
BlankNode,
Formula,
NamedNode,
Statement
} from 'rdflib'
import './dataContentPane.css'
const ns = UI.ns
// RDF icon — https://www.svgrepo.com/show/362270/rdf.svg (public domain via SVG Repo Mixer)
const RDF_ICON =
'data:image/svg+xml;utf8,' +
encodeURIComponent(
// Tightened viewBox: original artwork bbox is x≈129 y≈227 w≈741 h≈800.
// Centring it in an 800×800 square gives a balanced icon with ~30px padding.
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="100 227 800 800">' +
'<path fill="currentColor" d="M704 227q-40 0 -74.5 20.5t-53.5 56.5q-23 43 -15 92l-2 -2v6q-1 9 -7 19q-7 15 -23 30q-20 19 -50.5 38t-57.5 28q-22 7 -40 7q-14 0 -24 -4l-8 -3l3 3l-8 -4q-35 -19 -73.5 -17.5t-72 22t-52 55.5t-17 74t22 72t55.5 52q40 21 83.5 16.5t76.5 -31.5l-1 2l7 -4q9 -3 21 -3q17 -1 39 5q27 8 59 25q48 26 70 56q15 19 16 37q-1 42 19.5 78.5t58.5 56.5q35 18 73.5 16.5t72 -22t52 -55.5t17 -74t-22 -72t-55.5 -51q-7 -4 -15 -7l3 -1l-5 -4q-6 -5 -11 -16q-8 -15 -12 -36q-6 -27 -7 -63t3 -63q4 -22 11 -37q5 -11 12 -18l5 -4h-5q41 -21 62 -62q19 -35 17.5 -73.5t-22 -71.5t-55.5 -52q-33 -18 -70 -17zM706 250q39 0 66 25v0q-15 -13 -44.5 -10t-58.5 22q8 8 8.5 20t-8 21.5t-21.5 9.5h-8l-2 -1q-12 -4 -21 8q-16 29 -16 56.5t15 42.5q-20 -19 -27 -46.5t0.5 -56t28.5 -51.5q18 -19 41 -29.5t47 -10.5zM605 491q11 0 18 3.5t14 7.5q13 7 27 11q11 11 20 33q11 33 12.5 84t-10.5 84q-9 22 -22 34q-20 9 -36 23q-14 5 -36 1q-34 -6 -81 -31q-49 -26 -73 -54q-14 -17 -17 -32q2 -18 -1 -37v1l1 -6q1 -8 7 -18q8 -13 24 -28q20 -18 50 -37q62 -38 103 -39zM279 525q39 0 66 25h-1q-14 -13 -44 -10t-58 22q7 8 7.5 20t-8 21.5t-20.5 9.5q-4 1 -8 0l-2 -1q-13 -4 -22 8q-15 29 -15.5 56.5t14.5 42.5q-20 -19 -26.5 -46.5t1 -56t28.5 -51.5q18 -19 41 -29.5t47 -10.5zM731 758q39 0 66 25v0q-14 -13 -44 -10t-59 22q8 8 8.5 20t-8 21.5t-21.5 9.5q-4 0 -8 -1h-2q-12 -4 -21 8q-16 29 -16 56.5t15 42.5q-20 -19 -27 -46.5t0.5 -56t28.5 -51.5q18 -19 41 -29.5t47 -10.5z"/>' +
'</svg>'
)
type SubjectTerm = NamedNode | BlankNode
type ObjectTerm = Statement['object'] | Formula
type RootSubjectsResult = {
roots: SubjectTerm[]
subjects: Record<string, Statement[]>
loopBreakers?: Record<string, unknown>
}
type DataContentPaneLike = {
statementsAsTables: (
sts: Statement[],
context: DataBrowserContext,
initialRoots?: SubjectTerm[]
) => HTMLElement
}
export const dataContentPane = {
icon: RDF_ICON,
name: 'dataContents',
audience: [ns.solid('Developer')],
label: function (subject: NamedNode, context: DataBrowserContext) {
if (
'http://www.w3.org/2007/ont/link#ProtocolEvent' in
context.session.store.findTypeURIs(subject)
) {
return null
}
const n = context.session.store.statementsMatching(
undefined,
undefined,
undefined,
subject
).length
if (n === 0) return null
return 'Data (' + n + ')'
},
/*
shouldGetFocus: function(subject) {
return store.whether(subject, UI.ns.rdf('type'), UI.ns.link('RDFDocument'))
},
*/
/* This code was generated by Generative AI (GPT-5.3-Codex in GitHub Copilot) based on the following prompt:
Only the class assignments of data-content-pane__nested-table--light and
data-content-pane__nested-table--dark
are the only things added by AI.
This is the comment I got from copilot, but suggested fix remove the dark
class as well The nested table shading logic checks
UI.utils.ancestor(newTable, 'TABLE') immediately after
creating newTable, but at that moment newTable typically has no
parent in the DOM yet. This makes parentTable null and will
always apply data-content-pane__nested-table--light, preventing the
intended alternating light/dark nested backgrounds.
Consider assigning the class after appending newTable into
its parent table, or pass the parent table (or current nesting
depth) into objectTree() so the decision can be made reliably.
can you help me fix this without removing the dark altering */
statementsAsTables: function statementsAsTables (
sts: Statement[],
context: DataBrowserContext,
initialRoots?: SubjectTerm[]
): HTMLElement {
const myDocument = context.dom
const rep = myDocument.createElement('section')
rep.classList.add('data-content', 'data-content-pane__root')
const sz = $rdf.Serializer(context.session.store)
const res = sz.rootSubjects(sts) as RootSubjectsResult
let roots = res.roots
const subjects = res.subjects
const loopBreakers = res.loopBreakers ?? {}
for (const x in loopBreakers) {
console.log('\tdataContentPane: loopbreaker:' + x)
}
const doneBnodes: Record<string, true | HTMLDListElement> = {}
const referencedBnodes: Record<string, true> = {}
function propertyTree (
subject: SubjectTerm,
nestingLevel = 0
): HTMLDListElement {
const rep = myDocument.createElement('dl')
rep.classList.add('property-list', 'data-content-pane__property-list')
let lastPred: string | null = null
const subjectStatements = subjects[sz.toStr(subject)]
if (!subjectStatements) {
rep.appendChild(myDocument.createTextNode('...'))
return rep
}
subjectStatements.sort()
for (const st of subjectStatements) {
if (st.predicate.uri !== lastPred) {
const dt = myDocument.createElement('dt')
dt.classList.add('pred', 'data-content-pane__predicate')
const anchor = myDocument.createElement('a')
anchor.setAttribute('href', st.predicate.uri)
anchor.addEventListener(
'click',
UI.widgets.openHrefInOutlineMode,
true
)
anchor.appendChild(
myDocument.createTextNode(
UI.utils.predicateLabelForXML(st.predicate)
)
)
dt.appendChild(anchor)
rep.appendChild(dt)
lastPred = st.predicate.uri
}
const dd = myDocument.createElement('dd')
dd.classList.add('obj', 'data-content-pane__value')
dd.appendChild(objectTree(st.object, nestingLevel + 1))
rep.appendChild(dd)
}
return rep
}
function objectTree (obj: ObjectTerm, nestingLevel = 0): Node {
let res: HTMLElement | Text
let anchor: HTMLAnchorElement
switch (obj.termType) {
case 'NamedNode':
anchor = myDocument.createElement('a')
anchor.setAttribute('href', obj.uri)
anchor.addEventListener(
'click',
UI.widgets.openHrefInOutlineMode,
true
)
anchor.appendChild(myDocument.createTextNode(UI.utils.label(obj)))
return anchor
case 'Literal':
if (!obj.datatype || !obj.datatype.uri) {
res = myDocument.createElement('div')
res.classList.add('data-content-pane__literal')
res.textContent = obj.value
return res
} else if (
obj.datatype.uri ===
'http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral'
) {
res = myDocument.createElement('div')
res.classList.add('embeddedXHTML')
res.innerHTML = DOMPurify.sanitize(obj.value)
return res
}
return myDocument.createTextNode(obj.value)
case 'BlankNode': {
if (obj.toNT() in doneBnodes) {
referencedBnodes[obj.toNT()] = true
const referenceAnchor = myDocument.createElement('a')
referenceAnchor.setAttribute('href', '#' + obj.toNT().slice(2))
referenceAnchor.setAttribute('class', 'bnodeRef')
referenceAnchor.textContent = '*' + obj.toNT().slice(3)
return referenceAnchor
}
doneBnodes[obj.toNT()] = true
const newList = propertyTree(obj, nestingLevel)
doneBnodes[obj.toNT()] = newList
if (nestingLevel % 2 === 1) {
newList.classList.add('data-content-pane__nested-table--light')
} else {
newList.classList.add('data-content-pane__nested-table--dark')
}
return newList
}
case 'Collection': {
const list = myDocument.createElement('ol')
list.setAttribute('class', 'collectionAsTables')
for (let i = 0; i < obj.elements.length; i++) {
const li = myDocument.createElement('li')
li.appendChild(objectTree(obj.elements[i] as ObjectTerm, nestingLevel + 1))
list.appendChild(li)
}
return list
}
case 'Graph':
res = (context.session.paneRegistry
.byName('dataContents') as DataContentPaneLike)
.statementsAsTables(obj.statements, context)
res.setAttribute('class', 'data-content-pane__nested-formula')
return res
case 'Variable':
return myDocument.createTextNode('?' + obj.uri)
}
throw new Error('Unhandled node type: ' + obj.termType)
}
if (initialRoots) {
roots = initialRoots.concat(
roots.filter(function (x: SubjectTerm) {
for (let i = 0; i < initialRoots.length; i++) {
if (x.sameTerm(initialRoots[i])) return false
}
return true
})
)
}
for (let i = 0; i < roots.length; i++) {
const block = myDocument.createElement('article')
block.classList.add(
i % 2 === 0 ? 'data-content-pane__row--even' : 'data-content-pane__row--odd',
'data-content-pane__subject-block'
)
const header = myDocument.createElement('header')
header.classList.add('data-content-pane__subject-cell')
const root = roots[i]
if (root.termType === 'BlankNode') {
header.appendChild(myDocument.createTextNode(UI.utils.label(root)))
} else {
header.appendChild(objectTree(root, 0))
}
block.appendChild(header)
block.appendChild(propertyTree(root, 0))
rep.appendChild(block)
}
for (const bNT in referencedBnodes) {
const list = doneBnodes[bNT]
if (list === true) continue
const anchor = myDocument.createElement('a')
anchor.setAttribute('id', bNT.slice(2))
anchor.setAttribute('class', 'bnodeDef')
anchor.textContent = bNT.slice(3) + ')'
list.insertBefore(anchor, list.firstChild)
}
return rep
},
render: function (
subject: NamedNode,
context: DataBrowserContext
): HTMLDivElement {
const myDocument = context.dom
function applyEnvironmentAttributes (element: HTMLDivElement): void {
const environment = (context.environment ?? {}) as Partial<RenderEnvironment>
element.dataset.layout = environment.layout ?? 'desktop'
element.dataset.theme = environment.theme ?? 'light'
element.dataset.inputMode = environment.inputMode ?? 'pointer'
}
function mainRendering () {
const kb = context.session.store
const sts = kb.statementsMatching(undefined, undefined, undefined, subject)
const initialRoots: SubjectTerm[] = []
if (kb.holds(subject, undefined, undefined, subject)) {
initialRoots.push(subject)
}
const ps = kb.any(subject, UI.ns.foaf('primaryTopic'), undefined, subject)
if (ps && (ps.termType === 'NamedNode' || ps.termType === 'BlankNode')) {
initialRoots.push(ps as SubjectTerm)
}
div.appendChild(
context.session.paneRegistry
.byName('dataContents')
.statementsAsTables(sts, context, initialRoots)
)
}
const div = myDocument.createElement('div')
div.classList.add('dataContentPane', 'data-content-pane')
applyEnvironmentAttributes(div)
mainRendering()
return div
}
}