Skip to content

Commit 01c5422

Browse files
authored
Merge pull request #402 from solid/improve-labels
Improve labels
2 parents 10a09b5 + d0f8144 commit 01c5422

7 files changed

Lines changed: 219 additions & 116 deletions

File tree

src/acl/access-controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { AccessGroups } from './access-groups'
99
import { DataBrowserContext } from 'pane-registry'
1010
import { shortNameForFolder } from './acl-control'
1111
import { currentUser } from '../authn/authn'
12-
import * as utils from '../utils.js'
12+
import * as utils from '../utils'
1313
import * as debug from '../debug'
1414
import ns from '../ns'
1515

src/acl/acl-control.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import * as ns from '../ns'
9-
import * as utils from '../utils.js'
9+
import * as utils from '../utils'
1010
import { getACLorDefault, getProspectiveHolder } from './acl'
1111
import { IndexedFormula, NamedNode } from 'rdflib'
1212
import { DataBrowserContext } from 'pane-registry'

src/authn/authn.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { PaneDefinition } from 'pane-registry'
2525
import { Signup } from './signup'
2626
import * as widgets from '../widgets'
2727
import * as ns from '../ns.js'
28-
import * as utils from '../utils.js'
28+
import * as utils from '../utils'
2929
import { alert } from '../log'
3030
import { AppDetails, AuthenticationContext } from './types'
3131
import * as debug from '../debug'

src/utils.js renamed to src/utils/index.js

Lines changed: 6 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
//
44
// This must load AFTER the rdflib.js and log-ext.js (or log.js).
55
//
6-
import * as log from './log'
7-
import { store } from './logic'
8-
import * as ns from './ns'
6+
import * as log from '../log'
7+
import { store } from '../logic'
8+
import * as ns from '../ns'
99
import * as rdf from 'rdflib' // pull in first avoid cross-refs
10+
import { label } from './label'
1011

1112
const UI = { log, ns, rdf, store }
1213

@@ -366,8 +367,8 @@ function getEyeFocus (element, instantly, isBottom, myWindow) {
366367
myWindow.scrollBy(
367368
0,
368369
elementPosY +
369-
element.clientHeight -
370-
(myWindow.scrollY + myWindow.innerHeight)
370+
element.clientHeight -
371+
(myWindow.scrollY + myWindow.innerHeight)
371372
)
372373
return
373374
}
@@ -495,112 +496,6 @@ function labelWithOntology (x, initialCap) {
495496
return label(x, initialCap)
496497
}
497498

498-
// This ubiquitous function returns the best label for a thing
499-
//
500-
// The hacks in this code make a major difference to the usability
501-
//
502-
// @returns string
503-
//
504-
function label (x, initialCap) {
505-
// x is an object
506-
function doCap (s) {
507-
// s = s.toString()
508-
if (initialCap) return s.slice(0, 1).toUpperCase() + s.slice(1)
509-
return s
510-
}
511-
function cleanUp (s1) {
512-
let s2 = ''
513-
if (s1.slice(-1) === '/') s1 = s1.slice(0, -1) // chop trailing slash
514-
for (let i = 0; i < s1.length; i++) {
515-
if (s1[i] === '_' || s1[i] === '-') {
516-
s2 += ' '
517-
continue
518-
}
519-
s2 += s1[i]
520-
if (
521-
i + 1 < s1.length &&
522-
s1[i].toUpperCase() !== s1[i] &&
523-
s1[i + 1].toLowerCase() !== s1[i + 1]
524-
) {
525-
s2 += ' '
526-
}
527-
}
528-
if (s2.slice(0, 4) === 'has ') s2 = s2.slice(4)
529-
return doCap(s2)
530-
}
531-
532-
// Hard coded known label predicates
533-
// @@ TBD: Add subproperties of rdfs:label
534-
535-
const kb = UI.store
536-
const lab1 =
537-
kb.any(x, UI.ns.ui('label')) || // Prioritize ui:label
538-
kb.any(x, UI.ns.link('message')) ||
539-
kb.any(x, UI.ns.vcard('fn')) ||
540-
kb.any(x, UI.ns.foaf('name')) ||
541-
kb.any(x, UI.ns.dct('title')) ||
542-
kb.any(x, UI.ns.dc('title')) ||
543-
kb.any(x, UI.ns.rss('title')) ||
544-
kb.any(x, UI.ns.contact('fullName')) ||
545-
kb.any(x, kb.sym('http://www.w3.org/2001/04/roadmap/org#name')) ||
546-
kb.any(x, UI.ns.cal('summary')) ||
547-
kb.any(x, UI.ns.foaf('nick')) ||
548-
kb.any(x, UI.ns.rdfs('label'))
549-
550-
if (lab1) {
551-
return doCap(lab1.value)
552-
}
553-
554-
// Default to label just generated from the URI
555-
556-
if (x.termType === 'BlankNode') {
557-
return '...'
558-
}
559-
if (x.termType === 'Collection') {
560-
return '(' + x.elements.length + ')'
561-
}
562-
let s = x.uri
563-
if (typeof s === 'undefined') return x.toString() // can't be a symbol
564-
// s = decodeURI(s) // This can crash is random valid @ signs are presentation
565-
// The idea was to clean up eg URIs encoded in query strings
566-
// Also encoded character in what was filenames like @ [] {}
567-
try {
568-
s = s
569-
.split('/')
570-
.map(decodeURIComponent)
571-
.join('/') // If it is properly encoded
572-
} catch (e) {
573-
// try individual decoding of ASCII code points
574-
for (let i = s.length - 3; i > 0; i--) {
575-
const hex = '0123456789abcefABCDEF' // The while upacks multiple layers of encoding
576-
while (
577-
s[i] === '%' &&
578-
hex.indexOf(s[i + 1]) >= 0 &&
579-
hex.indexOf(s[i + 2]) >= 0
580-
) {
581-
s =
582-
s.slice(0, i) +
583-
String.fromCharCode(parseInt(s.slice(i + 1, i + 3), 16)) +
584-
s.slice(i + 3)
585-
}
586-
}
587-
}
588-
if (s.slice(-5) === '#this') s = s.slice(0, -5)
589-
else if (s.slice(-3) === '#me') s = s.slice(0, -3)
590-
591-
const hash = s.indexOf('#')
592-
if (hash >= 0) return cleanUp(s.slice(hash + 1))
593-
594-
if (s.slice(-9) === '/foaf.rdf') s = s.slice(0, -9)
595-
else if (s.slice(-5) === '/foaf') s = s.slice(0, -5)
596-
597-
// Eh? Why not do this? e.g. dc:title needs it only trim URIs, not rdfs:labels
598-
const slash = s.lastIndexOf('/', s.length - 2) // (len-2) excludes trailing slash
599-
if (slash >= 0 && slash < x.uri.length) return cleanUp(s.slice(slash + 1))
600-
601-
return doCap(decodeURIComponent(x.uri))
602-
}
603-
604499
function escapeForXML (str) {
605500
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;')
606501
}

src/utils/label.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import * as log from '../log'
2+
import { store } from '../logic'
3+
import * as ns from '../ns'
4+
import * as rdf from 'rdflib' // pull in first avoid cross-refs
5+
6+
const UI = { log, ns, rdf, store }
7+
8+
// This ubiquitous function returns the best label for a thing
9+
//
10+
// The hacks in this code make a major difference to the usability
11+
export function label (thing, initialCap = false): string {
12+
function doCap (label: string) {
13+
if (initialCap) {
14+
return label.slice(0, 1).toUpperCase() + label.slice(1)
15+
}
16+
return label
17+
}
18+
function cleanUp (label: string) {
19+
let result = ''
20+
if (label.slice(-1) === '/') label = label.slice(0, -1) // chop trailing slash
21+
for (let i = 0; i < label.length; i++) {
22+
if (label[i] === '_' || label[i] === '-') {
23+
result += ' '
24+
continue
25+
}
26+
result += label[i]
27+
if (
28+
i + 1 < label.length &&
29+
label[i].toUpperCase() !== label[i] &&
30+
label[i + 1].toLowerCase() !== label[i + 1]
31+
) {
32+
result += ' '
33+
}
34+
}
35+
if (result.slice(0, 4) === 'has ') result = result.slice(4)
36+
return doCap(result)
37+
}
38+
39+
const label = getWellKnownLabel(thing)
40+
41+
if (label) {
42+
return doCap(label.value)
43+
}
44+
45+
// Default to label just generated from the URI
46+
47+
if (thing.termType === 'BlankNode') {
48+
return '...'
49+
}
50+
if (thing.termType === 'Collection') {
51+
return '(' + thing.elements.length + ')'
52+
}
53+
let s = thing.uri
54+
if (typeof s === 'undefined') return thing.toString() // can't be a symbol
55+
// s = decodeURI(s) // This can crash is random valid @ signs are presentation
56+
// The idea was to clean up eg URIs encoded in query strings
57+
// Also encoded character in what was filenames like @ [] {}
58+
try {
59+
s = s
60+
.split('/')
61+
.map(decodeURIComponent)
62+
.join('/') // If it is properly encoded
63+
} catch (e) {
64+
// try individual decoding of ASCII code points
65+
for (let i = s.length - 3; i > 0; i--) {
66+
const hex = '0123456789abcefABCDEF' // The while upacks multiple layers of encoding
67+
while (
68+
s[i] === '%' &&
69+
hex.indexOf(s[i + 1]) >= 0 &&
70+
hex.indexOf(s[i + 2]) >= 0
71+
) {
72+
s =
73+
s.slice(0, i) +
74+
String.fromCharCode(parseInt(s.slice(i + 1, i + 3), 16)) +
75+
s.slice(i + 3)
76+
}
77+
}
78+
}
79+
80+
s = slice(s, '/profile/card#me')
81+
s = slice(s, '#this')
82+
s = slice(s, '#me')
83+
84+
const hash = s.indexOf('#')
85+
if (hash >= 0) return cleanUp(s.slice(hash + 1))
86+
87+
// Eh? Why not do this? e.g. dc:title needs it only trim URIs, not rdfs:labels
88+
const slash = s.lastIndexOf('/', s.length - 2) // (len-2) excludes trailing slash
89+
if (slash >= 0 && slash < thing.uri.length) return cleanUp(s.slice(slash + 1))
90+
91+
return doCap(decodeURIComponent(thing.uri))
92+
}
93+
94+
function slice (s: string, suffix: string) {
95+
const length = suffix.length * -1
96+
if (s.slice(length) === suffix) {
97+
return s.slice(0, length)
98+
}
99+
return s
100+
}
101+
102+
// Hard coded known label predicates
103+
// @@ TBD: Add subproperties of rdfs:label
104+
function getWellKnownLabel (thing) {
105+
return store.any(thing, UI.ns.ui('label')) || // Prioritize ui:label
106+
store.any(thing, UI.ns.link('message')) ||
107+
store.any(thing, UI.ns.vcard('fn')) ||
108+
store.any(thing, UI.ns.foaf('name')) ||
109+
store.any(thing, UI.ns.dct('title')) ||
110+
store.any(thing, UI.ns.dc('title')) ||
111+
store.any(thing, UI.ns.rss('title')) ||
112+
store.any(thing, UI.ns.contact('fullName')) ||
113+
store.any(thing, store.sym('http://www.w3.org/2001/04/roadmap/org#name')) ||
114+
store.any(thing, UI.ns.cal('summary')) ||
115+
store.any(thing, UI.ns.foaf('nick')) ||
116+
store.any(thing, UI.ns.as('name')) ||
117+
store.any(thing, UI.ns.schema('name')) ||
118+
store.any(thing, UI.ns.rdfs('label'))
119+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { silenceDebugMessages } from '../helpers/setup'
1+
import { silenceDebugMessages } from '../../helpers/setup'
22
import { JSDOM } from 'jsdom'
33
import {
44
addLoadEvent, // not used anywhere
@@ -29,7 +29,7 @@ import {
2929
stackString,
3030
syncTableToArray,
3131
syncTableToArrayReOrdered
32-
} from '../../src/utils'
32+
} from '../../../src/utils'
3333
import { sym } from 'rdflib'
3434

3535
silenceDebugMessages()

test/unit/utils/label.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { lit, sym } from 'rdflib'
2+
import { label } from '../../../src/utils'
3+
import { silenceDebugMessages } from '../../helpers/setup'
4+
import { store } from '../../../src/logic'
5+
import * as ns from '../../../src/ns'
6+
silenceDebugMessages()
7+
jest.mock('solid-auth-client', () => ({
8+
currentSession: () => Promise.resolve(),
9+
trackSession: () => null
10+
}))
11+
describe('label', () => {
12+
describe('uses well known label predicates', () => {
13+
const thing = sym('https://resource.example/label-test#it')
14+
15+
beforeEach(() => {
16+
store.removeDocument(thing.doc())
17+
})
18+
19+
it.each([
20+
'http://www.w3.org/ns/ui#label',
21+
'http://www.w3.org/2007/ont/link#message',
22+
'http://www.w3.org/2006/vcard/ns#fn',
23+
'http://xmlns.com/foaf/0.1/name',
24+
'http://purl.org/dc/terms/title',
25+
'http://purl.org/dc/elements/1.1/title',
26+
'http://purl.org/rss/1.0/title',
27+
'http://www.w3.org/2000/10/swap/pim/contact#fullName',
28+
'http://www.w3.org/2001/04/roadmap/org#name',
29+
'http://www.w3.org/2002/12/cal/ical#summary',
30+
'http://xmlns.com/foaf/0.1/nick',
31+
'http://www.w3.org/2000/01/rdf-schema#label',
32+
'https://www.w3.org/ns/activitystreams#name',
33+
'http://schema.org/name'
34+
])('renders %s as label', (property) => {
35+
store.add(thing, sym(property), lit('the label'), thing.doc())
36+
const result = label(thing)
37+
expect(result).toEqual('the label')
38+
})
39+
})
40+
41+
describe('when no label predicates are present', () => {
42+
it('the hostname is used when no path is present', () => {
43+
const result = label(sym('https://resource.example/'))
44+
expect(result).toEqual('resource.example')
45+
})
46+
it('the last part of the path is used if no fragment is present', () => {
47+
const result = label(sym('https://resource.example/path/to/folder/'))
48+
expect(result).toEqual('folder')
49+
})
50+
it('the filename is used if no fragment is present', () => {
51+
const result = label(sym('https://resource.example/path/to/folder/file.ttl'))
52+
expect(result).toEqual('file.ttl')
53+
})
54+
it('the fragment is used', () => {
55+
const result = label(
56+
sym('https://resource.example/path/to/folder/file.ttl#fragment')
57+
)
58+
expect(result).toEqual('fragment')
59+
})
60+
it('the last part of the path is used if fragment is #this', () => {
61+
const result = label(
62+
sym('https://resource.example/path/to/folder/#this')
63+
)
64+
expect(result).toEqual('folder')
65+
})
66+
it('the last part of the path is used if fragment is #me', () => {
67+
const result = label(
68+
sym('https://resource.example/path/to/folder/#me')
69+
)
70+
expect(result).toEqual('folder')
71+
})
72+
it('the hostname is used for common WebID URI pattern', () => {
73+
const result = label(sym('https://alice.solid.example/profile/card#me'))
74+
expect(result).toEqual('alice.solid.example')
75+
})
76+
})
77+
78+
describe('cleanup', () => {
79+
it('replaces dashes a underscores with blanks', () => {
80+
const result = label(sym('https://resource.example/path/to/some-weired_folder-name'))
81+
expect(result).toEqual('some weired folder name')
82+
})
83+
84+
it('separates camel case parts with blanks', () => {
85+
const result = label(sym('https://resource.example/path/to/camelCaseFolderName/'))
86+
expect(result).toEqual('camel Case Folder Name')
87+
})
88+
})
89+
})

0 commit comments

Comments
 (0)