@graphy/content.trig.write
Version:
RDF TriG content writer for dynamic and stylized output
676 lines (573 loc) • 19.5 kB
JavaScript
// queueMicrotask shim
{
// not defined or not a function
if('function' !== typeof queueMicrotask) {
// create resolved promise
let dp_resolve = Promise.resolve();
// try to redefine
try {
// eslint-disable-next-line no-global-assign
queueMicrotask = fk => dp_resolve.then(fk)
.catch(e_callback => setTimeout(() => {
throw e_callback;
}, 0));
}
// oh well, at least we tried
catch(e_define) {}
}
}
const factory = require('@graphy/core.data.factory');
const Writable = require('@graphy/core.class.writable');
// eslint-disable-next-line no-misleading-character-class
const RT_PREFIXED_NAME_NAMESPACE_VALID = /^([A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u{02ff}\u{0370}-\u{037d}\u{037f}-\u{1fff}\u{200c}-\u{200d}\u{2070}-\u{218f}\u{2c00}-\u{2fef}\u{3001}-\u{d7ff}\u{f900}-\u{fdcf}\u{fdf0}-\u{fffd}\u{10000}-\u{effff}]([A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u{02ff}\u{0370}-\u{037d}\u{037f}-\u{1fff}\u{200c}-\u{200d}\u{2070}-\u{218f}\u{2c00}-\u{2fef}\u{3001}-\u{d7ff}\u{f900}-\u{fdcf}\u{fdf0}-\u{fffd}\u{10000}-\u{effff}_\-0-9\xb7\u{0300}-\u{036f}\u{203f}-\u{2040}.]*[A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u{02ff}\u{0370}-\u{037d}\u{037f}-\u{1fff}\u{200c}-\u{200d}\u{2070}-\u{218f}\u{2c00}-\u{2fef}\u{3001}-\u{d7ff}\u{f900}-\u{fdcf}\u{fdf0}-\u{fffd}\u{10000}-\u{effff}_\-0-9\xb7\u{0300}-\u{036f}\u{203f}-\u{2040}])?)?$/u;
const N_MAX_STRING_BUFFER = 1 << 12;
const XC_DIRECTIVES_TYPE_SPARQL = 0b001;
const XC_DIRECTIVES_CASE_PASCAL = 0b010;
const XC_DIRECTIVES_CASE_UPPER = 0b100;
class TriG_Writer extends Writable {
constructor(gc_writer={}) {
super(gc_writer);
let {
prefixes: h_prefixes={},
lists: gc_lists=null,
debug: b_debug=false,
style: gc_style=null,
} = gc_writer;
Object.assign(this, {
_b_debug: b_debug,
_s_indent: '\t',
_b_simplify_default_graph: false,
_xc_directives: 0,
_s_token_prefix: '@prefix',
});
let s_graph_keyword = '';
// style config
if(gc_style) {
// 'graph' keyword
let z_graph_keyword = gc_style.graph_keyword || gc_style.graphKeyword || gc_style['graph-keyword'];
if(z_graph_keyword) {
// boolean true
if(true === z_graph_keyword) {
s_graph_keyword = 'GRAPH ';
}
// invalid type
else if('string' !== typeof z_graph_keyword) {
throw new TypeError(`Invalid argument type given for 'graph' token: ${z_graph_keyword}`);
}
// invalid token string
else if(!/^graph$/i.test(z_graph_keyword)) {
throw new Error(`Graph token must equal case-insensitive "GRAPH"; found: "${z_graph_keyword}"`);
}
// valid graph token; append space
else {
s_graph_keyword = z_graph_keyword+' ';
}
}
// default graph simplification
let w_simplify_default_graph = gc_style.simplify_default_graph || gc_style.simplifyDefaultGraph || gc_style['simplify-default-graph'];
if(w_simplify_default_graph) {
this._b_simplify_default_graph = !!w_simplify_default_graph;
}
// indent
if(gc_style.indent) {
this._s_indent = gc_style.indent.replace(/[^\s]/g, '');
}
// use sparql directives
let z_directives = gc_style.directives || gc_style.directives;
if(z_directives) {
switch(z_directives) {
case 'sparql': {
this._xc_directives = XC_DIRECTIVES_TYPE_SPARQL;
this._s_token_prefix = 'prefix';
break;
}
case 'Sparql': {
this._xc_directives = XC_DIRECTIVES_TYPE_SPARQL | XC_DIRECTIVES_CASE_PASCAL;
this._s_token_prefix = 'Prefix';
break;
}
case 'SPARQL': {
this._xc_directives = XC_DIRECTIVES_TYPE_SPARQL | XC_DIRECTIVES_CASE_UPPER;
this._s_token_prefix = 'PREFIX';
break;
}
case 'turtle': {
break;
}
case 'Turtle': {
this._xc_directives = XC_DIRECTIVES_CASE_PASCAL;
this._s_token_prefix = '@Prefix';
break;
}
case 'TURTLE': {
this._xc_directives = XC_DIRECTIVES_CASE_UPPER;
this._s_token_prefix = '@PREFIX';
break;
}
default: {
throw new Error(`Value not understood for 'directives' option: ${z_directives}`);
}
}
}
}
// set graph token
this._s_graph_keyword = s_graph_keyword;
// custom list keys
if(gc_lists) {
// serialize list object
this._serialize_list_object = function(a_list, n_nest_level) {
// transcode list object
let hc2_transcoded = this._transcode_list(a_list);
// serialize object
return this._encode_objects(hc2_transcoded, n_nest_level);
};
}
// serialize initial prefix mappings
let s_token_prefix = this._s_token_prefix;
let s_prefix_eol = (this._xc_directives & XC_DIRECTIVES_TYPE_SPARQL)? '\n': ' .\n';
let s_prefixes = '';
try {
// each user-defined prefix
for(let s_prefix_id in h_prefixes) {
// invalid prefix id
if(!RT_PREFIXED_NAME_NAMESPACE_VALID.test(s_prefix_id)) {
throw new Error(`Invlalid prefix id for application/trig RDF serialization format: '${s_prefix_id}'`);
}
// append to string
s_prefixes += `${s_token_prefix} ${s_prefix_id}: ${factory.namedNode(h_prefixes[s_prefix_id]).verbose()}${s_prefix_eol}`;
}
}
// serialization error
catch(e_serialize) {
queueMicrotask(() => {
this.emit('error', e_serialize);
});
}
// push prefixes
if(s_prefixes) this.push(s_prefixes);
}
// serialize prefixes
_serialize_prefixes(h_prefixes) {
// build prefixes string
let s_prefixes = (2 === this._xc_state)? '\n\n': '';
// update state
this._xc_state = 0;
// clone prefixes
this._h_prefixes = {...this._h_prefixes};
// ref prefix token
let s_token_prefix = this._s_token_prefix;
// prep eol string
let s_prefix_eol = (this._xc_directives & XC_DIRECTIVES_TYPE_SPARQL)? '\n': ' .\n';
// each user-defined prefix
for(let s_prefix_id in h_prefixes) {
// invalid prefix id
if(!RT_PREFIXED_NAME_NAMESPACE_VALID.test(s_prefix_id)) {
throw new Error(`Invlalid prefix id for application/trig RDF serialization format: '${s_prefix_id}'`);
}
// append to string
s_prefixes += `${s_token_prefix} ${s_prefix_id}: ${factory.namedNode(h_prefixes[s_prefix_id]).verbose()}${s_prefix_eol}`;
// set prefix
this._h_prefixes[s_prefix_id] = h_prefixes[s_prefix_id];
}
// recache
factory.cache_prefixes(this._h_prefixes);
// return prefix string
return s_prefixes;
}
// serialize c3 hash
_serialize_c3(hc3_triples) {
let {
_h_prefixes: h_prefixes,
_s_indent: s_indent,
} = this;
// break line if non-data state
let s_write = 2 !== this._xc_state? '\n': '';
// update state
this._xc_state = 2;
// triple delimiter
let s_delim_triples = '';
// subject exit listener
let f_exit_subject = null;
// each subject
for(let sc1_subject in hc3_triples) {
// directive
if('`' === sc1_subject[0]) {
let g_apply = this._apply_directive(sc1_subject, hc3_triples[sc1_subject]);
// write data
if(g_apply.write) {
s_write += s_delim_triples+g_apply.write;
// do not break next line
s_delim_triples = '';
}
// save exit listener
if(g_apply.exit) f_exit_subject = g_apply.exit;
continue;
}
// position before subject
let i_triples = s_write.length;
// serialize subject
s_write += s_delim_triples+factory.c1_node(sc1_subject, h_prefixes).terse(h_prefixes)+' ';
// pair indent & terminator
let s_indent_pairs = '';
let s_term_pairs = '';
// ref pairs
let hc2_pairs = hc3_triples[sc1_subject];
// position before pairs
let i_pairs = s_write.length;
// were objects written?
let b_empty = true;
// predicate exit listener
let f_exit_predicate = null;
// each predicate
for(let sc1_predicate in hc2_pairs) {
// directive
if('`' === sc1_predicate[0]) {
// apply directive
let g_apply = this._apply_directive(sc1_predicate, hc2_pairs[sc1_predicate]);
// write data
if(g_apply.write) {
// break line
s_write += (s_indent_pairs? s_term_pairs: '\n')+s_indent+g_apply.write;
// pair already terminated
s_term_pairs = '';
// indent next pair
s_indent_pairs = s_indent;
}
// save exit listener
if(g_apply.exit) f_exit_predicate = g_apply.exit;
continue;
}
// ref objects
let z_objects = hc2_pairs[sc1_predicate];
// serialize objects
let st_objects = this._encode_objects(z_objects);
// no objects; skip pair
if(!st_objects) continue;
// not empty
b_empty = false;
// cannot use blank node in predicate position
if('_' === sc1_predicate[0] && ':' === sc1_predicate[1]) {
throw new Error(`Cannot use blank node in predicate position of c3 hash; subject:'${sc1_subject}', predicate:'${sc1_predicate}'`);
}
// create predicate
let kt_predicate = factory.c1_named_node(sc1_predicate, h_prefixes);
// tersify rdf:type
let st_predicate = kt_predicate.isRdfTypeAlias? 'a': kt_predicate.terse(h_prefixes);
// serialize predicate and object(s)
s_write += s_term_pairs+s_indent_pairs+st_predicate+' '+st_objects;
// update state
this._xc_state = 2;
// // string buffer became too large
// if(s_write.length >= N_MAX_STRING_BUFFER) {
// debugger;
// }
// terminate next pair
s_term_pairs = ' ;\n';
// indent next pair
s_indent_pairs = s_indent;
// call exit predicate listener
if(f_exit_predicate) f_exit_predicate();
}
// empty triples; cut out
if(b_empty) {
s_write = s_write.slice(0, i_triples)+s_write.slice(i_pairs);
continue;
}
// delimit triple(s)
s_delim_triples = '\n';
// close triple
s_write += `${s_term_pairs? ' ': s_indent_pairs}.\n`; //
// call exit subject listener
if(f_exit_subject) f_exit_subject();
}
s_write += '\n';
return s_write;
}
// serialize c4 hash
_serialize_c4(hc4_quads) {
let {
_h_prefixes: h_prefixes,
_s_indent: s_indent,
} = this;
// break line if non-data state
let s_write = 2 !== this._xc_state? '\n': '';
// update state
this._xc_state = 2;
// force default graph brace
let b_simplify_default_graph = this._b_simplify_default_graph;
// graph token
let s_graph_keyword = this._s_graph_keyword;
// graph exit listener
let f_exit_graph = null;
// each graph
for(let sc1_graph in hc4_quads) {
// directive
if('`' === sc1_graph[0]) {
let g_apply = this._apply_directive(sc1_graph, hc4_quads[sc1_graph]);
// write data
if(g_apply.write) s_write += g_apply.write;
// save exit listener
if(g_apply.exit) f_exit_graph = g_apply.exit;
continue;
}
// serialize open graph
let st_graph = factory.c1_node(sc1_graph, h_prefixes).terse(h_prefixes);
s_write += st_graph
? s_graph_keyword+st_graph+' {\n'
: (b_simplify_default_graph? '': s_graph_keyword+'{\n');
// simplify default graph implies no indent
let s_indent_root = (!st_graph && b_simplify_default_graph)? '': s_indent;
// update state
this._xc_state = 2;
// ref triples
let hc3_triples = hc4_quads[sc1_graph];
// triple delimiter
let s_delim_triples = '';
// subject exit listener
let f_exit_subject = null;
// each subject
for(let sc1_subject in hc3_triples) {
// directive
if('`' === sc1_subject[0]) {
let g_apply = this._apply_directive(sc1_subject, hc3_triples[sc1_subject]);
// write data
if(g_apply.write) {
s_write += s_delim_triples+s_indent_root+g_apply.write;
// do not break next line
s_delim_triples = '';
}
// save exit listener
if(g_apply.exit) f_exit_subject = g_apply.exit;
continue;
}
// position before subject
let i_triples = s_write.length;
// serialize subject
s_write += s_delim_triples+s_indent_root+factory.c1_node(sc1_subject, h_prefixes).terse(h_prefixes)+' ';
// pair indent & terminator
let s_indent_pairs = '';
let s_term_pairs = '';
// ref pairs
let hc2_pairs = hc3_triples[sc1_subject];
// position before pairs
let i_pairs = s_write.length;
// were objects written?
let b_empty = true;
// predicate exit listener
let f_exit_predicate = null;
// each predicate
for(let sc1_predicate in hc2_pairs) {
// directive
if('`' === sc1_predicate[0]) {
// apply directive
let g_apply = this._apply_directive(sc1_predicate, hc2_pairs[sc1_predicate]);
// write data
if(g_apply.write) {
// break line
s_write += (s_indent_pairs? s_term_pairs: '\n')+s_indent+s_indent_root+g_apply.write;
// pair already terminated
s_term_pairs = '';
// indent next pair
s_indent_pairs = s_indent+s_indent_root;
}
// save exit listener
if(g_apply.exit) f_exit_predicate = g_apply.exit;
continue;
}
// ref objects
let z_objects = hc2_pairs[sc1_predicate];
// serialize objects
let st_objects = this._encode_objects(z_objects);
// no objects; skip pair
if(!st_objects) continue;
// not empty
b_empty = false;
// cannot use blank node in predicate position
if('_' === sc1_predicate[0] && ':' === sc1_predicate[1]) {
throw new Error(`Cannot use blank node in predicate position of c4 hash; graph:'${sc1_graph}', subject:'${sc1_subject}', predicate:'${sc1_predicate}'`);
}
// create predicate
let kt_predicate = factory.c1_named_node(sc1_predicate, h_prefixes);
// tersify rdf:type
let st_predicate = kt_predicate.isRdfTypeAlias? 'a': kt_predicate.terse(h_prefixes);
// serialize predicate and object(s)
s_write += s_term_pairs+s_indent_pairs+st_predicate+' '+st_objects;
// update state
this._xc_state = 2;
// // string buffer became too large
// if(s_write.length >= N_MAX_STRING_BUFFER) {
// debugger;
// }
// terminate next pair
s_term_pairs = ' ;\n';
// indent next pair
s_indent_pairs = s_indent+s_indent_root;
// call exit predicate listener
if(f_exit_predicate) f_exit_predicate();
}
// empty triples; cut out
if(b_empty) {
s_write = s_write.slice(0, i_triples)+s_write.slice(i_pairs);
continue;
}
// delimit triple(s)
s_delim_triples = '\n';
// close triple
s_write += `${s_term_pairs? ' ': s_indent_pairs}.\n`; // \n
// call exit subject listener
if(f_exit_subject) f_exit_subject();
}
// close graph
s_write += ((st_graph || !b_simplify_default_graph)? '}\n': '')+'\n';
// call exit graph listener
if(f_exit_graph) f_exit_graph();
}
return s_write;
}
// write objects
_encode_objects(z_objects, n_nest_level=1) {
let {
_h_prefixes: h_prefixes,
_s_indent: s_indent,
_hm_coercions: hm_coercions,
} = this;
// deduce object value type
switch(typeof z_objects) {
// concise-term string
case 'string': return factory.c1(z_objects, h_prefixes).terse(h_prefixes);
// numeric type
case 'number': return factory.number(z_objects).terse(h_prefixes);
// boolean type
case 'boolean': return factory.boolean(z_objects).terse(h_prefixes);
// object
case 'object': {
// null; reject
if(null === z_objects) throw new Error('Refusing to serialize null value given as an object of quad');
// array, list of objects
if(Array.isArray(z_objects) || z_objects instanceof Set) {
let s_write = '';
// object terminator
let s_term_object = '';
// each object
for(let z_item of z_objects) {
// item is an array; serialize list
if(Array.isArray(z_item)) {
s_write += s_term_object + this._serialize_list_object(z_item, n_nest_level);
}
// non-array
else {
// recurse on item
s_write += s_term_object + this._encode_objects(z_item, n_nest_level);
}
// terminate next object
s_term_object = ', ';
}
return s_write;
}
// plain object, blank node
else if(Object === z_objects.constructor) {
// open blank node block
let s_write = '[';
// whether the block is empty
let b_empty = true;
// object exit listener
let f_exit_object = null;
// each pair
for(let sc1_predicate in z_objects) {
// block is not empty
b_empty = false;
// terminate previous pair
s_write += '\n'+s_indent.repeat(2+n_nest_level);
// directive; serialize it
if('`' === sc1_predicate[0]) {
let g_apply = this._apply_directive(sc1_predicate, z_objects[sc1_predicate]);
// write data
if(g_apply.write) s_write += g_apply.write;
// save exit listener
if(g_apply.exit) f_exit_object = g_apply.exit;
continue;
}
// write predicate and object(s)
s_write += factory.c1(sc1_predicate, h_prefixes).terse(h_prefixes) + ' '
+ this._encode_objects(z_objects[sc1_predicate], n_nest_level+1) +' ;';
}
// close blank node block
s_write += (b_empty? '': '\n'+s_indent.repeat(1+n_nest_level))+']';
// call exit object listener
if(f_exit_object) f_exit_object();
// serialize current predicate to blank node
return s_write;
}
// coercable instance
else if(hm_coercions.has(z_objects.constructor)) {
// convert javascript object to term object
let kt_converted = hm_coercions.get(z_objects.constructor).apply(this, [z_objects, n_nest_level]);
// serialize
return kt_converted.terse(h_prefixes);
}
// graphy term
else if(z_objects.isGraphyTerm) {
return z_objects.terse(h_prefixes);
}
// RDFJS term
else if(z_objects.termType) {
return factory.from.term(z_objects).terse(h_prefixes);
}
}
// fallthrough: other
default: {
throw new Error(`Bad type for RDF object: [${typeof z_objects}] ${z_objects? z_objects.constructor: z_objects}`);
}
}
}
// serialize collection object
_serialize_collection_object(a_collection, n_nest_level) {
let s_indent = this._s_indent;
// open collection block
let s_write = '(';
// each item
for(let z_item of a_collection) {
let s_objects = '';
// item is array; serialize as sub-collection
if(Array.isArray(z_item)) {
s_objects = this._serialize_collection_object(z_item, n_nest_level+1);
}
// non-array item
else {
s_objects = this._encode_objects(z_item, n_nest_level+1);
}
// serialize collection
s_write += '\n'+s_indent.repeat(2+n_nest_level)+s_objects;
}
// break line if anything was written (including comments)
if(a_collection.length) s_write += '\n'+s_indent.repeat(1+n_nest_level);
// close collection block
s_write += ')';
return s_write;
}
// rdfjs quad
_serialize_quad(g_quad) {
let h_prefixes = this._h_prefixes;
let kq_quad = factory.from.quad(g_quad);
let st_graph = kq_quad.graph.terse(h_prefixes);
// serialize quad
this._s_push += (2 !== this._xc_state? '\n': '')
+this._s_graph_keyword+(st_graph? st_graph+' ': '')+'{\n\t'
+kq_quad.subject.terse(h_prefixes)+' '
+kq_quad.predicate.terse(h_prefixes)+' '
+kq_quad.object.terse(h_prefixes)+' .\n'
+'}\n\n';
// update state
this._xc_state = 2;
}
}
Object.assign(TriG_Writer.prototype, {
anonymous_blank_nodes: true,
_serialize_c3r: TriG_Writer.prototype._serialize_c3,
_serialize_c4r: TriG_Writer.prototype._serialize_c4,
_serialize_comment: Writable.prototype._serialize_hash_comment,
_serialize_list_object: TriG_Writer.prototype._serialize_collection_object,
});
module.exports = function(gc_writer) {
return new TriG_Writer(gc_writer);
};