rdflib
Version:
an RDF library for node.js. Suitable for client and server side.
333 lines (300 loc) • 10.4 kB
JavaScript
/**
* Utility functions for $rdf
* @module util
*/
import { docpart } from './uri'
import log from './log'
import * as uri from './uri'
import NamedNode from './named-node'
const string = { template: stringTemplate }
export { log, uri, string }
export function mediaTypeClass(mediaType){
mediaType = mediaType.split(';')[0].trim() // remove media type parameters
return new NamedNode('http://www.w3.org/ns/iana/media-types/' + mediaType + '#Resource')
}
export function linkRelationProperty(relation){
return new NamedNode('http://www.w3.org/ns/iana/link-relations/relation#' + relation.trim())
}
/**
* Adds callback functionality to an object.
* Callback functions are indexed by a 'hook' string.
* They return true if they want to be called again.
* @method callbackify
* @param obj {Object}
* @param callbacks {Array<string>}
*/
export function callbackify (obj, callbacks) {
obj.callbacks = {}
for (var x = callbacks.length - 1; x >= 0; x--) {
obj.callbacks[callbacks[x]] = []
}
obj.addHook = function (hook) {
if (!obj.callbacks[hook]) {
obj.callbacks[hook] = []
}
}
obj.addCallback = function (hook, func) {
obj.callbacks[hook].push(func)
}
obj.removeCallback = function (hook, funcName) {
for (var i = 0;i < obj.callbacks[hook].length;i++) {
if (obj.callbacks[hook][i].name === funcName) {
obj.callbacks[hook].splice(i, 1)
return true
}
}
return false
}
obj.insertCallback = function (hook, func) {
obj.callbacks[hook].unshift(func)
}
obj.fireCallbacks = function fireCallbacks (hook, args) {
var newCallbacks = []
var replaceCallbacks = []
var len = obj.callbacks[hook].length
var x
let callback
// log.info('!@$ Firing '+hook+' call back with length'+len)
for (x = len - 1; x >= 0; x--) {
// log.info('@@ Firing '+hook+' callback '+ obj.callbacks[hook][x])
callback = obj.callbacks[hook][x]
if (callback && callback.apply(obj, args)) {
newCallbacks.push(callback)
}
}
for (x = newCallbacks.length - 1; x >= 0; x--) {
replaceCallbacks.push(newCallbacks[x])
}
for (x = len; x < obj.callbacks[hook].length; x++) {
replaceCallbacks.push(obj.callbacks[hook][x])
}
obj.callbacks[hook] = replaceCallbacks
}
}
/**
* Returns a DOM parser based on current runtime environment.
*/
export function DOMParserFactory () {
if (window.DOMParser) {
return new DOMParser()
} else if (window.ActiveXObject) {
return new ActiveXObject('Microsoft.XMLDOM')
} else {
return false
}
}
// From https://github.com/linkeddata/dokieli
export function domToString (node, options) {
options = options || {}
var selfClosing = []
if (options && options.selfClosing) {
options.selfClosing.split(' ').forEach(function (n) {
selfClosing[n] = true
})
}
var skipAttributes = []
if (options && options.skipAttributes) {
options.skipAttributes.split(' ').forEach(function (n) {
skipAttributes[n] = true
})
}
return dumpNode(node, options, selfClosing, skipAttributes)
}
export function dumpNode (node, options, selfClosing, skipAttributes) {
var i
var out = ''
var noEsc = [ false ]
if (typeof node.nodeType === 'undefined') return out
if (node.nodeType === 1) {
if (node.hasAttribute('class') && (options && options.classWithChildText) && node.matches(options.classWithChildText.class)) {
out += node.querySelector(options.classWithChildText.element).textContent
} else if (!((options && options.skipNodeWithClass) && node.matches('.' + options.skipNodeWithClass))) {
var ename = node.nodeName.toLowerCase()
out += '<' + ename
var attrList = []
for (i = node.attributes.length - 1; i >= 0; i--) {
var atn = node.attributes[i]
if (skipAttributes && skipAttributes.length > 0 && skipAttributes[atn.name]) continue
if (/^\d+$/.test(atn.name)) continue
if (atn.name === 'class' && (options && options.replaceClassItemWith) && (atn.value.split(' ').indexOf(options.replaceClassItemWith.source) > -1)) {
var re = new RegExp(options.replaceClassItemWith.source, 'g')
atn.value = atn.value.replace(re, options.replaceClassItemWith.target).trim()
}
if (!(atn.name === 'class' && (options && options.skipClassWithValue) && options.skipClassWithValue === atn.value)) {
attrList.push(atn.name + '=\'' + atn.value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/'/g, '"') + '\'')
}
}
if (attrList.length > 0) {
if (options && options.sortAttributes) {
attrList.sort(function (a, b) {
return a.toLowerCase().localeCompare(b.toLowerCase())
})
}
out += ' ' + attrList.join(' ')
}
if (selfClosing && selfClosing.ename) {
out += ' />'
} else {
out += '>'
out += (ename === 'html') ? '\n ' : ''
noEsc.push(ename === 'style' || ename === 'script')
for (i = 0; i < node.childNodes.length; i++) out += dumpNode(node.childNodes[i])
noEsc.pop()
out += (ename === 'body') ? '</' + ename + '>' + '\n' : '</' + ename + '>'
}
}
} else if (node.nodeType === 8) {
// FIXME: If comments are not tabbed in source, a new line is not prepended
out += '<!--' + node.nodeValue + '-->'
} else if (node.nodeType === 3 || node.nodeType === 4) {
// XXX: Remove new lines which were added after DOM ready
var nl = node.nodeValue.replace(/\n+$/, '')
out += noEsc[noEsc.length - 1] ? nl : nl.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
} else {
console.log('Warning; Cannot handle serialising nodes of type: ' + node.nodeType)
console.log(node)
}
return out
}
export function dtstamp () {
var now = new Date()
var year = now.getYear() + 1900
var month = now.getMonth() + 1
var day = now.getDate()
var hour = now.getUTCHours()
var minute = now.getUTCMinutes()
var second = now.getSeconds()
if (month < 10) month = '0' + month
if (day < 10) day = '0' + day
if (hour < 10) hour = '0' + hour
if (minute < 10) minute = '0' + minute
if (second < 10) second = '0' + second
return year + '-' + month + '-' + day + 'T' +
hour + ':' + minute + ':' + second + 'Z'
}
/**
* Compares statements (heavy comparison for repeatable canonical ordering)
*/
export function heavyCompare (x, y, g, uriMap) {
var nonBlank = function (x) {
return (x.termType === 'BlankNode') ? null : x
}
var signature = function (x) {
var lis = g.statementsMatching(x).map(function (st) {
return ('' + nonBlank(st.subject) + ' ' + nonBlank(st.predicate) +
' ' + nonBlank(st.object))
}).concat(g.statementsMatching(undefined, undefined, x).map(function (st) {
return ('' + nonBlank(st.subject) + ' ' + nonBlank(st.predicate) +
' ' + nonBlank(st.object))
}))
lis.sort()
return lis.join('\n')
}
const comparison = Object.prototype.hasOwnProperty.call(g, "compareTerms")
? g.compareTerms(x, y)
: x.compareTerm(y)
if ((x.termType === 'BlankNode') && (y.termType === 'BlankNode')) {
if (comparison === 0) return 0 // Same
if (signature(x) > signature(y)) return +1
if (signature(x) < signature(y)) return -1
return comparison // Too bad -- this order not canonical.
// throw "different bnodes indistinquishable for sorting"
} else {
if (uriMap && x.uri && y.uri){
return (uriMap[x.uri] || x.uri).localeCompare(uriMap[y.uri] || y.uri)
}
return comparison
}
}
export function heavyCompareSPO (x, y, g, uriMap) {
return heavyCompare(x.subject, y.subject, g, uriMap) ||
heavyCompare(x.predicate, y.predicate, g, uriMap) ||
heavyCompare(x.object, y.object, g, uriMap)
}
/**
* Defines a simple debugging function
* @method output
* @param o {String}
*/
export function output (o) {
var k = document.createElement('div')
k.textContent = o
document.body.appendChild(k)
}
import { DOMParser } from '@xmldom/xmldom'
/**
* Returns a DOM from parsed XML.
*/
export function parseXML (str, options) {
var dparser
options = options || {}
if (typeof module !== 'undefined' && module && module.exports) { // Node.js
var dom = new DOMParser().parseFromString(str, options.contentType || 'application/xhtml+xml')
return dom
} else {
if (typeof window !== 'undefined' && window.DOMParser) {
dparser = new window.DOMParser() // seems to actually work
} else {
dparser = new DOMParser() // Doc says this works
}
}
return dparser.parseFromString(str, 'application/xml')
}
/**
* Removes all statements equal to x from a
*/
export function RDFArrayRemove (a, x) {
for (var i = 0; i < a.length; i++) {
// TODO: This used to be the following, which didnt always work..why
// if(a[i] === x)
if (a[i].subject.equals(x.subject) &&
a[i].predicate.equals(x.predicate) &&
a[i].object.equals(x.object) &&
a[i].why.equals(x.why)) {
a.splice(i, 1)
return
}
}
throw new Error('RDFArrayRemove: Array did not contain ' + x + ' ' + x.why)
}
export function string_startswith (str, pref) { // missing library routines
return (str.slice(0, pref.length) === pref)
}
/**
* C++, python style %s -> subs
*/
function stringTemplate (base, subs) {
var baseA = base.split('%s')
var result = ''
for (var i = 0;i < subs.length;i++) {
subs[i] += ''
result += baseA[i] + subs[i]
}
return result + baseA.slice(subs.length).join()
}
// Stack dump on errors - to pass errors back
export function stackString (e) {
var str = '' + e + '\n'
if (!e.stack) {
return str + 'No stack available.\n'
}
var lines = e.stack.toString().split('\n')
var toprint = []
for (var i = 0; i < lines.length; i++) {
var line = lines[i]
if (line.indexOf('ecmaunit.js') > -1) {
// remove useless bit of traceback
break
}
if (line.charAt(0) == '(') {
line = 'function' + line
}
var chunks = line.split('@')
toprint.push(chunks)
}
// toprint.reverse(); No - I prefer the latest at the top by the error message -tbl
for (var i = 0; i < toprint.length; i++) {
str += ' ' + toprint[i][1] + '\n ' + toprint[i][0]
}
return str
}