www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

serialize.js (31860B)


      1 /*      Serialization of RDF Graphs
      2  **
      3  ** Tim Berners-Lee 2006
      4  ** This is or was http://dig.csail.mit.edu/2005/ajar/ajaw/js/rdf/serialize.js
      5  **
      6  ** Bug: can't serialize  http://data.semanticweb.org/person/abraham-bernstein/rdf
      7  ** in XML (from mhausenblas)
      8  */
      9 // @@@ Check the whole toStr thing tosee whetehr it still makes sense -- tbl
     10 // 
     11 $rdf.Serializer = function () {
     12 
     13   var __Serializer = function (store) {
     14       this.flags = "";
     15       this.base = null;
     16       this.prefixes = [];
     17       this.keywords = ['a']; // The only one we generate at the moment
     18       this.prefixchars = "abcdefghijklmnopqustuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
     19       this.incoming = null; // Array not calculated yet
     20       this.formulas = []; // remebering original formulae from hashes 
     21       this.store = store;
     22 
     23       /* pass */
     24     }
     25 
     26   var Serializer = function (store) {
     27       return new __Serializer(store)
     28     };
     29 
     30   __Serializer.prototype.setBase = function (base) {
     31     this.base = base
     32   };
     33 
     34   __Serializer.prototype.setFlags = function (flags) {
     35     this.flags = flags ? flags : ''
     36   };
     37 
     38 
     39   __Serializer.prototype.toStr = function (x) {
     40     var s = x.toNT();
     41     if(x.termType == 'formula') {
     42       this.formulas[s] = x; // remember as reverse does not work
     43     }
     44     return s;
     45   };
     46 
     47   __Serializer.prototype.fromStr = function (s) {
     48     if(s[0] == '{') {
     49       var x = this.formulas[s];
     50       if(!x) alert('No formula object for ' + s)
     51       return x;
     52     }
     53     return this.store.fromNT(s);
     54   };
     55 
     56 
     57 
     58 
     59 
     60   /* Accumulate Namespaces
     61    **
     62    ** These are only hints.  If two overlap, only one gets used
     63    ** There is therefore no guarantee in general.
     64    */
     65 
     66   __Serializer.prototype.suggestPrefix = function (prefix, uri) {
     67     this.prefixes[uri] = prefix;
     68   }
     69 
     70   // Takes a namespace -> prefix map
     71   __Serializer.prototype.suggestNamespaces = function (namespaces) {
     72     for(var px in namespaces) {
     73       this.prefixes[namespaces[px]] = px;
     74     }
     75   }
     76 
     77   // Make up an unused prefix for a random namespace
     78   __Serializer.prototype.makeUpPrefix = function (uri) {
     79     var p = uri;
     80     var namespaces = [];
     81     var pok;
     82     var sz = this;
     83 
     84     function canUse(pp) {
     85       if(namespaces[pp]) return false; // already used
     86       sz.prefixes[uri] = pp;
     87       pok = pp;
     88       return true
     89     }
     90     for(var ns in sz.prefixes) {
     91       namespaces[sz.prefixes[ns]] = ns; // reverse index
     92     }
     93     // trim off illegal characters from the end
     94     var i;
     95     for(i = p.length - 1; i>=0; i--) {
     96       if(sz._notNameChars.indexOf(p.charAt(i)) == -1) break;
     97     }
     98     p = p.substring(0, i+1);
     99     if(p) {
    100       // find shortest possible NCName to use as namespace name
    101       for(i = p.length - 1; i>=0; i--) {
    102         if(sz._notNameChars.indexOf(p.charAt(i)) != -1) break;
    103       }
    104       i++;
    105       p = p.substr(i);
    106       
    107       if(p.length < 6 && canUse(p)) return pok; // exact is best
    108       if(canUse(p.slice(0, 3))) return pok;
    109       if(canUse(p.slice(0, 2))) return pok;
    110       if(canUse(p.slice(0, 4))) return pok;
    111       if(canUse(p.slice(0, 1))) return pok;
    112       if(canUse(p.slice(0, 5))) return pok;
    113       p = p.slice(0, 3);
    114     } else {
    115       // no suitable characters (weird), fall back to 'ns'
    116       p = 'ns';
    117       if(canUse(p)) return pok;
    118     }
    119     for(var i = 0;; i++) if(canUse(p + i)) return pok;
    120   }
    121 
    122 
    123 
    124   // Todo:
    125   //  - Sort the statements by subject, pred, object
    126   //  - do stuff about the docu first and then (or first) about its primary topic.
    127   __Serializer.prototype.rootSubjects = function (sts) {
    128     var incoming = {};
    129     var subjects = {};
    130     var sz = this;
    131     var allBnodes = {};
    132 
    133     /* This scan is to find out which nodes will have to be the roots of trees
    134      ** in the serialized form. This will be any symbols, and any bnodes
    135      ** which hve more or less than one incoming arc, and any bnodes which have
    136      ** one incoming arc but it is an uninterrupted loop of such nodes back to itself.
    137      ** This should be kept linear time with repect to the number of statements.
    138      ** Note it does not use any indexing of the store.
    139      */
    140 
    141 
    142     tabulator.log.debug('serialize.js Find bnodes with only one incoming arc\n')
    143     for(var i = 0; i < sts.length; i++) {
    144       var st = sts[i];
    145       [st.subject, st.predicate, st.object].map(function (y) {
    146         if(y.termType == 'bnode') {
    147           allBnodes[y.toNT()] = true
    148         }
    149       });
    150       var x = sts[i].object;
    151       if(!incoming[x]) incoming[x] = [];
    152       incoming[x].push(st.subject) // List of things which will cause this to be printed
    153       var ss = subjects[sz.toStr(st.subject)]; // Statements with this as subject
    154       if(!ss) ss = [];
    155       ss.push(st);
    156       subjects[this.toStr(st.subject)] = ss; // Make hash. @@ too slow for formula?
    157       //$rdf.log.debug(' sz potential subject: '+sts[i].subject)
    158     }
    159 
    160     var roots = [];
    161     for(var xNT in subjects) {
    162       var x = sz.fromStr(xNT);
    163       if((x.termType != 'bnode') || !incoming[x] || (incoming[x].length != 1)) {
    164         roots.push(x);
    165         //$rdf.log.debug(' sz actual subject -: ' + x)
    166         continue;
    167       }
    168     }
    169     this.incoming = incoming; // Keep for serializing @@ Bug for nested formulas
    170     //////////// New bit for CONNECTED bnode loops:frootshash
    171     // This scans to see whether the serialization is gpoing to lead to a bnode loop
    172     // and at the same time accumulates a list of all bnodes mentioned.
    173     // This is in fact a cut down N3 serialization
    174     /*
    175     tabulator.log.debug('serialize.js Looking for connected bnode loops\n')
    176     for (var i=0; i<sts.length; i++) { // @@TBL
    177         // dump('\t'+sts[i]+'\n');
    178     }
    179     var doneBnodesNT = {};
    180     function dummyPropertyTree(subject, subjects, rootsHash) {
    181         // dump('dummyPropertyTree('+subject+'...)\n');
    182         var sts = subjects[sz.toStr(subject)]; // relevant statements
    183         for (var i=0; i<sts.length; i++) {
    184             dummyObjectTree(sts[i].object, subjects, rootsHash);
    185         }
    186     }
    187 
    188     // Convert a set of statements into a nested tree of lists and strings
    189     // @param force,    "we know this is a root, do it anyway. It isn't a loop."
    190     function dummyObjectTree(obj, subjects, rootsHash, force) { 
    191         // dump('dummyObjectTree('+obj+'...)\n');
    192         if (obj.termType == 'bnode' && (subjects[sz.toStr(obj)]  &&
    193             (force || (rootsHash[obj.toNT()] == undefined )))) {// and there are statements
    194             if (doneBnodesNT[obj.toNT()]) { // Ah-ha! a loop
    195                 throw "Serializer: Should be no loops "+obj;
    196             }
    197             doneBnodesNT[obj.toNT()] = true;
    198             return  dummyPropertyTree(obj, subjects, rootsHash);
    199         }
    200         return dummyTermToN3(obj, subjects, rootsHash);
    201     }
    202     
    203     // Scan for bnodes nested inside lists too
    204     function dummyTermToN3(expr, subjects, rootsHash) {
    205         if (expr.termType == 'bnode') doneBnodesNT[expr.toNT()] = true;
    206         tabulator.log.debug('serialize: seen '+expr);
    207         if (expr.termType == 'collection') {
    208             for (i=0; i<expr.elements.length; i++) {
    209                 if (expr.elements[i].termType == 'bnode')
    210                     dummyObjectTree(expr.elements[i], subjects, rootsHash);
    211             }
    212         return;             
    213         }
    214     }
    215 
    216     // The tree for a subject
    217     function dummySubjectTree(subject, subjects, rootsHash) {
    218         // dump('dummySubjectTree('+subject+'...)\n');
    219         if (subject.termType == 'bnode' && !incoming[subject])
    220             return dummyObjectTree(subject, subjects, rootsHash, true); // Anonymous bnode subject
    221         dummyTermToN3(subject, subjects, rootsHash);
    222         dummyPropertyTree(subject, subjects, rootsHash);
    223     }
    224 */
    225     // Now do the scan using existing roots
    226     tabulator.log.debug('serialize.js Dummy serialize to check for missing nodes')
    227     var rootsHash = {};
    228     for(var i = 0; i < roots.length; i++) rootsHash[roots[i].toNT()] = true;
    229     /*
    230     for (var i=0; i<roots.length; i++) {
    231         var root = roots[i];
    232         dummySubjectTree(root, subjects, rootsHash);
    233     }
    234     // dump('Looking for mising bnodes...\n')
    235     
    236 // Now in new roots for anythig not acccounted for
    237 // Now we check for any bndoes which have not been covered.
    238 // Such bnodes must be in isolated rings of pure bnodes.
    239 // They each have incoming link of 1.
    240 
    241     tabulator.log.debug('serialize.js Looking for connected bnode loops\n')
    242     for (;;) {
    243         var bnt;
    244         var found = null;
    245         for (bnt in allBnodes) { // @@ Note: not repeatable. No canonicalisation
    246             if (doneBnodesNT[bnt]) continue;
    247             found = bnt; // Ah-ha! not covered
    248             break;
    249         }
    250         if (found == null) break; // All done - no bnodes left out/
    251         // dump('Found isolated bnode:'+found+'\n');
    252         doneBnodesNT[bnt] = true;
    253         var root = this.store.fromNT(found);
    254         roots.push(root); // Add a new root
    255         rootsHash[found] = true;
    256         tabulator.log.debug('isolated bnode:'+found+', subjects[found]:'+subjects[found]+'\n');
    257         if (subjects[found] == undefined) {
    258             for (var i=0; i<sts.length; i++) {
    259                 // dump('\t'+sts[i]+'\n');
    260             }
    261             throw "Isolated node should be a subject" +found;
    262         }
    263         dummySubjectTree(root, subjects, rootsHash); // trace out the ring
    264     }
    265     // dump('Done bnode adjustments.\n')
    266 */
    267     return {
    268       'roots': roots,
    269       'subjects': subjects,
    270       'rootsHash': rootsHash,
    271       'incoming': incoming
    272     };
    273   }
    274 
    275   ////////////////////////////////////////////////////////
    276   __Serializer.prototype.toN3 = function (f) {
    277     return this.statementsToN3(f.statements);
    278   }
    279 
    280   __Serializer.prototype._notQNameChars = "\t\r\n !\"#$%&'()*,+/;<=>?@[\\]^`{|}~";
    281   __Serializer.prototype._notNameChars = (__Serializer.prototype._notQNameChars + ":");
    282   __Serializer.prototype._NCNameRegExp = (function() {
    283     // escape characters that are unsafe inside RegExp character set
    284     var reSafeChars = __Serializer.prototype._notNameChars.replace(/[-\]\\]/g, '\\$&');
    285     return new RegExp('[^0-9\\-.' + reSafeChars + '][^' + reSafeChars + ']*$');
    286   })();
    287 
    288 
    289   __Serializer.prototype.statementsToN3 = function (sts) {
    290     var indent = 4;
    291     var width = 80;
    292     var sz = this;
    293 
    294     var namespaceCounts = []; // which have been used
    295     var predMap = {
    296       'http://www.w3.org/2002/07/owl#sameAs': '=',
    297       'http://www.w3.org/2000/10/swap/log#implies': '=>',
    298       'http://www.w3.org/1999/02/22-rdf-syntax-ns#type': 'a'
    299     }
    300 
    301 
    302 
    303 
    304     ////////////////////////// Arrange the bits of text 
    305     var spaces = function (n) {
    306         var s = '';
    307         for(var i = 0; i < n; i++) s += ' ';
    308         return s
    309       }
    310 
    311     var treeToLine = function (tree) {
    312         var str = '';
    313         for(var i = 0; i < tree.length; i++) {
    314           var branch = tree[i];
    315           var s2 = (typeof branch == 'string') ? branch : treeToLine(branch);
    316           if(i != 0 && s2 != ',' && s2 != ';' && s2 != '.') str += ' ';
    317           str += s2;
    318         }
    319         return str;
    320       }
    321 
    322       // Convert a nested tree of lists and strings to a string
    323     var treeToString = function (tree, level) {
    324         var str = '';
    325         var lastLength = 100000;
    326         if(!level) level = 0;
    327         for(var i = 0; i < tree.length; i++) {
    328           var branch = tree[i];
    329           if(typeof branch != 'string') {
    330             var substr = treeToString(branch, level + 1);
    331             if(substr.length < 10 * (width - indent * level)
    332               && substr.indexOf('"""') < 0) {
    333               // Don't mess up multiline strings
    334               var line = treeToLine(branch);
    335               if(line.length < (width - indent * level)) {
    336                 branch = '   ' + line; //   @@ Hack: treat as string below
    337                 substr = ''
    338               }
    339             }
    340             if(substr) lastLength = 10000;
    341             str += substr;
    342           }
    343           if(typeof branch == 'string') {
    344             if(branch.length == '1' && str.slice(-1) == '\n') {
    345               if(",.;".indexOf(branch) >= 0) {
    346                 str = str.slice(0, -1) + branch + '\n'; //  slip punct'n on end
    347                 lastLength += 1;
    348                 continue;
    349               } else if("])}".indexOf(branch) >= 0) {
    350                 str = str.slice(0, -1) + ' ' + branch + '\n';
    351                 lastLength += 2;
    352                 continue;
    353               }
    354             }
    355             if(lastLength < (indent * level + 4)) { // continue
    356               str = str.slice(0, -1) + ' ' + branch + '\n';
    357               lastLength += branch.length + 1;
    358             } else {
    359               var line = spaces(indent * level) + branch;
    360               str += line + '\n';
    361               lastLength = line.length;
    362             }
    363 
    364           } else { // not string
    365           }
    366         }
    367         return str;
    368       };
    369 
    370     ////////////////////////////////////////////// Structure for N3
    371 
    372     // Convert a set of statements into a nested tree of lists and strings
    373     function statementListToTree(statements) {
    374       // print('Statement tree for '+statements.length);
    375       var res = [];
    376       var stats = sz.rootSubjects(statements);
    377       var roots = stats.roots;
    378       var results = []
    379       for(var i = 0; i < roots.length; i++) {
    380         var root = roots[i];
    381         results.push(subjectTree(root, stats))
    382       }
    383       return results;
    384     }
    385 
    386     // The tree for a subject
    387     function subjectTree(subject, stats) {
    388       if(subject.termType == 'bnode' && !stats.incoming[subject])
    389         return objectTree(subject, stats, true).concat(["."]); // Anonymous bnode subject
    390       return [termToN3(subject, stats)].concat([propertyTree(subject, stats)]).concat(["."]);
    391     }
    392 
    393 
    394     // The property tree for a single subject or anonymous node
    395     function propertyTree(subject, stats) {
    396       // print('Proprty tree for '+subject);
    397       var results = []
    398       var lastPred = null;
    399       var sts = stats.subjects[sz.toStr(subject)]; // relevant statements
    400       if(typeof sts == 'undefined') {
    401         throw('Cant find statements for ' + subject);
    402       }
    403       sts.sort();
    404       var objects = [];
    405       for(var i = 0; i < sts.length; i++) {
    406         var st = sts[i];
    407         if(st.predicate.uri == lastPred) {
    408           objects.push(',');
    409         } else {
    410           if(lastPred) {
    411             results = results.concat([objects]).concat([';']);
    412             objects = [];
    413           }
    414           results.push(predMap[st.predicate.uri] ?
    415             predMap[st.predicate.uri] :
    416             termToN3(st.predicate, stats));
    417         }
    418         lastPred = st.predicate.uri;
    419         objects.push(objectTree(st.object, stats));
    420       }
    421       results = results.concat([objects]);
    422       return results;
    423     }
    424 
    425     function objectTree(obj, stats, force) {
    426       if(obj.termType == 'bnode'
    427         && stats.subjects[sz.toStr(obj)]
    428         // and there are statements
    429         && (force || stats.rootsHash[obj.toNT()] == undefined)) // and not a root
    430         return ['['].concat(propertyTree(obj, stats)).concat([']']);
    431       return termToN3(obj, stats);
    432     }
    433 
    434     function termToN3(expr, stats) {
    435       switch(expr.termType) {
    436       case 'bnode':
    437       case 'variable':
    438         return expr.toNT();
    439       case 'literal':
    440         var str = stringToN3(expr.value);
    441         if(expr.lang) str += '@' + expr.lang;
    442         if(expr.datatype) str += '^^' + termToN3(expr.datatype, stats);
    443         return str;
    444       case 'symbol':
    445         return symbolToN3(expr.uri);
    446       case 'formula':
    447         var res = ['{'];
    448         res = res.concat(statementListToTree(expr.statements));
    449         return res.concat(['}']);
    450       case 'collection':
    451         var res = ['('];
    452         for(i = 0; i < expr.elements.length; i++) {
    453           res.push([objectTree(expr.elements[i], stats)]);
    454         }
    455         res.push(')');
    456         return res;
    457 
    458       default:
    459         throw "Internal: termToN3 cannot handle " + expr + " of termType+" + expr.termType
    460         return '' + expr;
    461       }
    462     }
    463 
    464     ////////////////////////////////////////////// Atomic Terms
    465     //  Deal with term level things and nesting with no bnode structure
    466     function symbolToN3(uri) { // c.f. symbolString() in notation3.py
    467       var j = uri.indexOf('#');
    468       if(j < 0 && sz.flags.indexOf('/') < 0) {
    469         j = uri.lastIndexOf('/');
    470       }
    471       if(j >= 0 && sz.flags.indexOf('p') < 0) { // Can split at namespace
    472         var canSplit = true;
    473         for(var k = j + 1; k < uri.length; k++) {
    474           if(__Serializer.prototype._notNameChars.indexOf(uri[k]) >= 0) {
    475             canSplit = false;
    476             break;
    477           }
    478         }
    479         if(canSplit) {
    480           var localid = uri.slice(j + 1);
    481           var namesp = uri.slice(0, j + 1);
    482           if(sz.defaultNamespace
    483             && sz.defaultNamespace == namesp
    484             && sz.flags.indexOf('d') < 0) { // d -> suppress default
    485             if(sz.flags.indexOf('k') >= 0
    486               && sz.keyords.indexOf(localid) < 0)
    487               return localid;
    488             return ':' + localid;
    489           }
    490           var prefix = sz.prefixes[namesp];
    491           if(prefix) {
    492             namespaceCounts[namesp] = true;
    493             return prefix + ':' + localid;
    494           }
    495           if(uri.slice(0, j) == sz.base)
    496             return '<#' + localid + '>';
    497           // Fall though if can't do qname
    498         }
    499       }
    500       if(sz.flags.indexOf('r') < 0 && sz.base)
    501         uri = $rdf.Util.uri.refTo(sz.base, uri);
    502       else if(sz.flags.indexOf('u') >= 0)
    503         uri = backslashUify(uri);
    504       else uri = hexify(uri);
    505       return '<' + uri + '>';
    506     }
    507 
    508     function prefixDirectives() {
    509       var str = '';
    510       if(sz.defaultNamespace)
    511         str += '@prefix : <' + sz.defaultNamespace + '>.\n';
    512       for(var ns in namespaceCounts) {
    513         str += '@prefix ' + sz.prefixes[ns] + ': <' + ns + '>.\n';
    514       }
    515       return str + '\n';
    516     }
    517 
    518     //  stringToN3:  String escaping for N3
    519     //
    520     var forbidden1 = new RegExp(/[\\"\b\f\r\v\t\n\u0080-\uffff]/gm);
    521     var forbidden3 = new RegExp(/[\\"\b\f\r\v\u0080-\uffff]/gm);
    522 
    523     function stringToN3(str, flags) {
    524       if(!flags) flags = "e";
    525       var res = '', i = 0, j = 0;
    526       var delim;
    527       var forbidden;
    528       if(str.length > 20 // Long enough to make sense
    529         && str.slice(-1) != '"' // corner case'
    530         && flags.indexOf('n') < 0 // Force single line
    531         && (str.indexOf('\n') > 0 || str.indexOf('"') > 0)) {
    532         delim = '"""';
    533         forbidden = forbidden3;
    534       } else {
    535         delim = '"';
    536         forbidden = forbidden1;
    537       }
    538       for(i = 0; i < str.length;) {
    539         forbidden.lastIndex = 0;
    540         var m = forbidden.exec(str.slice(i));
    541         if(m == null) break;
    542         j = i + forbidden.lastIndex - 1;
    543         res += str.slice(i, j);
    544         var ch = str[j];
    545         if(ch == '"' && delim == '"""' && str.slice(j, j + 3) != '"""') {
    546           res += ch;
    547         } else {
    548           var k = '\b\f\r\t\v\n\\"'.indexOf(ch); // No escaping of bell (7)?
    549           if(k >= 0) {
    550             res += "\\" + 'bfrtvn\\"' [k];
    551           } else {
    552             if(flags.indexOf('e') >= 0) {
    553               res += '\\u' + ('000' + ch.charCodeAt(0).toString(16).toLowerCase()).slice(-4)
    554             } else { // no 'e' flag
    555               res += ch;
    556             }
    557           }
    558         }
    559         i = j + 1;
    560       }
    561       return delim + res + str.slice(i) + delim
    562     }
    563 
    564     // Body of toN3:
    565     var tree = statementListToTree(sts);
    566     return prefixDirectives() + treeToString(tree, -1);
    567 
    568   }
    569 
    570   // String ecaping utilities 
    571   function hexify(str) { // also used in parser
    572     //     var res = '';
    573     //     for (var i=0; i<str.length; i++) {
    574     //         k = str.charCodeAt(i);
    575     //         if (k>126 || k<33)
    576     //             res += '%' + ('0'+n.toString(16)).slice(-2); // convert to upper?
    577     //         else
    578     //             res += str[i];
    579     //     }
    580     //     return res;
    581     return encodeURI(str);
    582   }
    583 
    584 
    585   function backslashUify(str) {
    586     var res = '', k;
    587     for(var i = 0; i < str.length; i++) {
    588       k = str.charCodeAt(i);
    589       if(k > 65535)
    590         res += '\\U' + ('00000000' + n.toString(16)).slice(-8); // convert to upper?
    591       else if(k > 126)
    592         res += '\\u' + ('0000' + n.toString(16)).slice(-4);
    593       else
    594         res += str[i];
    595     }
    596     return res;
    597   }
    598 
    599 
    600 
    601 
    602 
    603 
    604   //////////////////////////////////////////////// XML serialization
    605   __Serializer.prototype.statementsToXML = function (sts) {
    606     var indent = 4;
    607     var width = 80;
    608     var sz = this;
    609 
    610     var namespaceCounts = []; // which have been used
    611     namespaceCounts['http://www.w3.org/1999/02/22-rdf-syntax-ns#'] = true;
    612 
    613     ////////////////////////// Arrange the bits of XML text 
    614     var spaces = function (n) {
    615         var s = '';
    616         for(var i = 0; i < n; i++) s += ' ';
    617         return s
    618       }
    619 
    620     var XMLtreeToLine = function (tree) {
    621         var str = '';
    622         for(var i = 0; i < tree.length; i++) {
    623           var branch = tree[i];
    624           var s2 = (typeof branch == 'string') ? branch : XMLtreeToLine(branch);
    625           str += s2;
    626         }
    627         return str;
    628       }
    629 
    630       // Convert a nested tree of lists and strings to a string
    631     var XMLtreeToString = function (tree, level) {
    632         var str = '';
    633         var lastLength = 100000;
    634         if(!level) level = 0;
    635         for(var i = 0; i < tree.length; i++) {
    636           var branch = tree[i];
    637           if(typeof branch != 'string') {
    638             var substr = XMLtreeToString(branch, level + 1);
    639             if(substr.length < 10 * (width - indent * level)
    640               && substr.indexOf('"""') < 0) {
    641               // Don't mess up multiline strings
    642               var line = XMLtreeToLine(branch);
    643               if(line.length < (width - indent * level)) {
    644                 branch = '   ' + line; //   @@ Hack: treat as string below
    645                 substr = ''
    646               }
    647             }
    648             if(substr) lastLength = 10000;
    649             str += substr;
    650           }
    651           if(typeof branch == 'string') {
    652             if(lastLength < (indent * level + 4)) { // continue
    653               str = str.slice(0, -1) + ' ' + branch + '\n';
    654               lastLength += branch.length + 1;
    655             } else {
    656               var line = spaces(indent * level) + branch;
    657               str += line + '\n';
    658               lastLength = line.length;
    659             }
    660 
    661           } else { // not string
    662           }
    663         }
    664         return str;
    665       };
    666 
    667     function statementListToXMLTree(statements) {
    668       sz.suggestPrefix('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
    669       var stats = sz.rootSubjects(statements);
    670       var roots = stats.roots;
    671       var results = [], root;
    672       for(var i = 0; i < roots.length; i++) {
    673         root = roots[i];
    674         results.push(subjectXMLTree(root, stats))
    675       }
    676       return results;
    677     }
    678 
    679     function escapeForXML(str) {
    680       if(typeof str == 'undefined') return '@@@undefined@@@@';
    681       return str.replace(/&/g, '&amp;')
    682         .replace(/</g, '&lt;')
    683         .replace(/>/g, '&gt;')
    684         .replace(/"/g, '&quot;');
    685     }
    686 
    687     function relURI(term) {
    688       return escapeForXML((sz.base) ? $rdf.Util.uri.refTo(this.base, term.uri) : term.uri);
    689     }
    690 
    691     // The tree for a subject
    692     function subjectXMLTree(subject, stats) {
    693       const liPrefix = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#_';
    694       var results = [];
    695       var type = null, t, st;
    696       var sts = stats.subjects[sz.toStr(subject)]; // relevant statements
    697       // Sort only on the predicate, leave the order at object
    698       // level undisturbed.  This leaves multilingual content in
    699       // the order of entry (for partner literals), which helps
    700       // readability.
    701       //
    702       // For the predicate sort, we attempt to split the uri
    703       // as a hint to the sequence, as sequenced items seems
    704       // to be of the form http://example.com#_1, http://example.com#_2,
    705       // et cetera.  Probably not the most optimal of fixes, but
    706       // it does work.
    707       sts.sort(function (a, b) {
    708         var aa = a.predicate.uri.split('#_');
    709         var bb = a.predicate.uri.split('#_');
    710         if(aa[0] > bb[0]) {
    711           return 1;
    712         } else if(aa[0] < bb[0]) {
    713           return -1;
    714         } else if("undefined" !== typeof aa[1] && "undefined" !== typeof bb[1]) {
    715           if(parseInt(aa[1], 10) > parseInt(bb[1], 10)) {
    716             return 1;
    717           } else if(parseInt(aa[1], 10) < parseInt(bb[1], 10)) {
    718             return -1;
    719           }
    720         }
    721         return 0;
    722       });
    723 
    724       for(var i = 0; i < sts.length; i++) {
    725         st = sts[i];
    726         if(st.predicate.uri == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' && !type && st.object.termType == "symbol") {
    727           // look for a type
    728           type = st.object;
    729         } else {
    730           // see whether predicate can be replaced with "li"
    731           if(st.predicate.uri.substr(0, liPrefix.length) == liPrefix) {
    732             var number = st.predicate.uri.substr(liPrefix.length);
    733             // make sure these are actually numeric list items
    734             var intNumber = parseInt(number);
    735             if(number == intNumber.toString()) {
    736               // was numeric; don't need to worry about ordering since we've already
    737               // sorted the statements
    738               st.predicate = $rdf.Symbol('http://www.w3.org/1999/02/22-rdf-syntax-ns#li');
    739             }
    740           }
    741           t = qname(st.predicate);
    742           switch(st.object.termType) {
    743           case 'bnode':
    744             if(sz.incoming[st.object].length == 1) {
    745               results = results.concat(['<' + t + '>',
    746                                                         subjectXMLTree(st.object, stats),
    747                                                         '</' + t + '>']);
    748             } else {
    749               results = results.concat(['<' + t + ' rdf:nodeID="'
    750                                                         + st.object.toNT().slice(2) + '"/>']);
    751             }
    752             break;
    753           case 'symbol':
    754             results = results.concat(['<' + t + ' rdf:resource="'
    755                                                   + relURI(st.object) + '"/>']);
    756             break;
    757           case 'literal':
    758             results = results.concat(['<' + t
    759                                                   + (st.object.dt ? ' rdf:datatype="' + escapeForXML(st.object.dt.uri) + '"' : '')
    760                                                   + (st.object.lang ? ' xml:lang="' + st.object.lang + '"' : '')
    761                                                   + '>' + escapeForXML(st.object.value)
    762                                                   + '</' + t + '>']);
    763             break;
    764           case 'collection':
    765             results = results.concat(['<' + t + ' rdf:parseType="Collection">',
    766                                                   collectionXMLTree(st.object, stats),
    767                                                   '</' + t + '>']);
    768             break;
    769           default:
    770             throw "Can't serialize object of type " + st.object.termType + " into XML";
    771           } // switch
    772         }
    773       }
    774 
    775       var tag = type ? qname(type) : 'rdf:Description';
    776 
    777       var attrs = '';
    778       if(subject.termType == 'bnode') {
    779         if(!sz.incoming[subject] || sz.incoming[subject].length != 1) { // not an anonymous bnode
    780           attrs = ' rdf:nodeID="' + subject.toNT().slice(2) + '"';
    781         }
    782       } else {
    783         attrs = ' rdf:about="' + relURI(subject) + '"';
    784       }
    785 
    786       return ['<' + tag + attrs + '>'].concat([results]).concat(["</" + tag + ">"]);
    787     }
    788 
    789     function collectionXMLTree(subject, stats) {
    790       var res = []
    791       for(var i = 0; i < subject.elements.length; i++) {
    792         res.push(subjectXMLTree(subject.elements[i], stats));
    793       }
    794       return res;
    795     }
    796 
    797     // The property tree for a single subject or anonymos node
    798     function propertyXMLTree(subject, stats) {
    799       var results = []
    800       var sts = stats.subjects[sz.toStr(subject)]; // relevant statements
    801       if(sts == undefined) return results; // No relevant statements
    802       sts.sort();
    803       for(var i = 0; i < sts.length; i++) {
    804         var st = sts[i];
    805         switch(st.object.termType) {
    806         case 'bnode':
    807           if(stats.rootsHash[st.object.toNT()]) { // This bnode has been done as a root -- no content here @@ what bout first time
    808             results = results.concat(['<' + qname(st.predicate) + ' rdf:nodeID="' + st.object.toNT().slice(2) + '">',
    809                                       '</' + qname(st.predicate) + '>']);
    810           } else {
    811             results = results.concat(['<' + qname(st.predicate) + ' rdf:parseType="Resource">',
    812                                       propertyXMLTree(st.object, stats),
    813                                       '</' + qname(st.predicate) + '>']);
    814           }
    815           break;
    816         case 'symbol':
    817           results = results.concat(['<' + qname(st.predicate) + ' rdf:resource="'
    818                                         + relURI(st.object) + '"/>']);
    819           break;
    820         case 'literal':
    821           results = results.concat(['<' + qname(st.predicate)
    822                                     + (st.object.datatype ? ' rdf:datatype="' + escapeForXML(st.object.datatype.uri) + '"' : '')
    823                                     + (st.object.lang ? ' xml:lang="' + st.object.lang + '"' : '')
    824                                     + '>' + escapeForXML(st.object.value)
    825                                     + '</' + qname(st.predicate) + '>']);
    826           break;
    827         case 'collection':
    828           results = results.concat(['<' + qname(st.predicate) + ' rdf:parseType="Collection">',
    829                                     collectionXMLTree(st.object, stats),
    830                                     '</' + qname(st.predicate) + '>']);
    831           break;
    832         default:
    833           throw "Can't serialize object of type " + st.object.termType + " into XML";
    834 
    835         } // switch
    836       }
    837       return results;
    838     }
    839 
    840     function qname(term) {
    841       var uri = term.uri;
    842 
    843       var j = uri.search(sz._NCNameRegExp);
    844       if(j < 0) throw("Cannot make qname out of <" + uri + ">")
    845       
    846       var localid = uri.substr(j);
    847       var namesp = uri.substr(0, j);
    848       if(sz.defaultNamespace
    849         && sz.defaultNamespace == namesp
    850         && sz.flags.indexOf('d') < 0) { // d -> suppress default
    851         return localid;
    852       }
    853       var prefix = sz.prefixes[namesp];
    854       if(!prefix) prefix = sz.makeUpPrefix(namesp);
    855       namespaceCounts[namesp] = true;
    856       return prefix + ':' + localid;
    857       //        throw ('No prefix for namespace "'+namesp +'" for XML qname for '+uri+', namespaces: '+sz.prefixes+' sz='+sz); 
    858     }
    859 
    860     // Body of toXML:
    861     var tree = statementListToXMLTree(sts);
    862     var str = '<rdf:RDF';
    863     if(sz.defaultNamespace)
    864       str += ' xmlns="' + escapeForXML(sz.defaultNamespace) + '"';
    865     for(var ns in namespaceCounts) {
    866       str += '\n xmlns:' + sz.prefixes[ns] + '="' + escapeForXML(ns) + '"';
    867     }
    868     str += '>';
    869 
    870     var tree2 = [str, tree, '</rdf:RDF>']; //@@ namespace declrations
    871     return XMLtreeToString(tree2, -1);
    872 
    873 
    874   } // End @@ body
    875   return Serializer;
    876 
    877 }();