// shex-simple - Simple ShEx2 validator for HTML. // Copyright 2017 Eric Prud'hommeux // Release under MIT License. const START_SHAPE_LABEL = "START"; const START_SHAPE_INDEX_ENTRY = "- start -"; // specificially not a JSON-LD @id form. const INPUTAREA_TIMEOUT = 250; const NO_MANIFEST_LOADED = "no manifest loaded"; var LOG_PROGRESS = false; var DefaultBase = location.origin + location.pathname; var Caches = {}; Caches.inputSchema = makeSchemaCache($("#inputSchema textarea.schema")); Caches.inputData = makeTurtleCache($("#inputData textarea")); Caches.manifest = makeManifestCache($("#manifestDrop")); Caches.shapeMap = makeShapeMapCache($("#textMap")); // @@ rename to #shapeMap var ShExRSchema; // defined below const ParseTriplePattern = (function () { const uri = "<[^>]*>|[a-zA-Z0-9_-]*:[a-zA-Z0-9_-]*"; const literal = "((?:" + "'(?:[^'\\\\]|\\\\')*'" + "|" + "\"(?:[^\"\\\\]|\\\\\")*\"" + "|" + "'''(?:(?:'|'')?[^'\\\\]|\\\\')*'''" + "|" + "\"\"\"(?:(?:\"|\"\")?[^\"\\\\]|\\\\\")*\"\"\"" + ")" + "(?:@[a-zA-Z-]+|\\^\\^(?:" + uri + "))?)"; const uriOrKey = uri + "|FOCUS|_"; // const termOrKey = uri + "|" + literal + "|FOCUS|_"; return "(\\s*{\\s*)("+ uriOrKey+")?(\\s*)("+ uri+"|a)?(\\s*)("+ uriOrKey+"|" + literal + ")?(\\s*)(})?(\\s*)"; })(); var Getables = [ {queryStringParm: "schema", location: Caches.inputSchema.selection, cache: Caches.inputSchema}, {queryStringParm: "data", location: Caches.inputData.selection, cache: Caches.inputData }, {queryStringParm: "manifest", location: Caches.manifest.selection, cache: Caches.manifest , fail: e => $("#manifestDrop li").text(NO_MANIFEST_LOADED)}, {queryStringParm: "shape-map", location: $("#textMap"), cache: Caches.shapeMap }, ]; var QueryParams = Getables.concat([ {queryStringParm: "interface", location: $("#interface"), deflt: "human" }, {queryStringParm: "regexpEngine", location: $("#regexpEngine"), deflt: "threaded-val-nerr" }, ]); // utility functions function parseTurtle (text, meta, base) { var ret = ShEx.N3.Store(); ShEx.N3.Parser._resetBlankNodeIds(); var parser = ShEx.N3.Parser({documentIRI: base, format: "text/turtle" }); var triples = parser.parse(text); if (triples !== undefined) ret.addTriples(triples); meta.base = parser._base; meta.prefixes = parser._prefixes; return ret; } var shexParser = ShEx.Parser.construct(DefaultBase); function parseShEx (text, meta, base) { shexParser._setOptions({duplicateShape: $("#duplicateShape").val()}); shexParser._setBase(base); var ret = shexParser.parse(text); // ret = ShEx.Util.canonicalize(ret, DefaultBase); meta.base = ret.base; meta.prefixes = ret.prefixes; return ret; } function sum (s) { // cheap way to identify identical strings return s.replace(/\s/g, "").split("").reduce(function (a,b){ a = ((a<<5) - a) + b.charCodeAt(0); return a&a },0); } // function rdflib_termToLex (node, resolver) { if (node === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type") return "a"; if (node === ShEx.Validator.start) return START_SHAPE_LABEL; if (node === resolver._base) return "<>"; if (node.indexOf(resolver._base) === 0/* && ['#', '?'].indexOf(node.substr(resolver._base.length)) !== -1 */) return "<" + node.substr(resolver._base.length) + ">"; if (node.indexOf(resolver._basePath) === 0 && ['#', '?', '/', '\\'].indexOf(node.substr(resolver._basePath.length)) === -1) return "<" + node.substr(resolver._basePath.length) + ">"; return ShEx.N3.Writer({ prefixes:resolver.meta.prefixes || {} })._encodeObject(node); } function rdflib_lexToTerm (lex, resolver) { return lex === START_SHAPE_LABEL ? ShEx.Validator.start : lex === "a" ? "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" : ShEx.N3.Lexer().tokenize(lex + " ") // need " " to parse "chat"@en .map(token => { var left = token.type === "typeIRI" ? "^^" : token.type === "langcode" ? "@" : token.type === "type" ? "^^" + resolver.meta.prefixes[token.prefix] : token.type === "prefixed" ? resolver.meta.prefixes[token.prefix] : token.type === "blank" ? "_:" : ""; var right = token.type === "IRI" || token.type === "typeIRI" ? resolver._resolveAbsoluteIRI(token) : token.value; return left + right; }).join(""); return lex === ShEx.Validator.start ? lex : lex[0] === "<" ? lex.substr(1, lex.length - 2) : lex; } // // caches for textarea parsers function _makeCache (selection) { var _dirty = true; var resolver; var ret = { selection: selection, parsed: null, meta: { prefixes: {}, base: DefaultBase }, dirty: function (newVal) { var ret = _dirty; _dirty = newVal; return ret; }, get: function () { return selection.val(); }, set: function (text, base) { _dirty = true; selection.val(text); this.meta.base = base; if (base !== DefaultBase) { this.url = base; // @@crappyHack1 -- parms should differntiate: // working base: base for URL resolution. // loaded base: place where you can GET current doc. // Note that Caches.manifest.set takes a 3rd parm. } }, refresh: function () { if (!_dirty) return this.parsed; this.parsed = this.parse(selection.val(), this.meta.base); resolver._setBase(this.meta.base); _dirty = false; return this.parsed; }, asyncGet: function (url) { var _cache = this; return new Promise(function (resolve, reject) { $.ajax({ accepts: { mycustomtype: 'text/shex,text/turtle,*/*' }, url: url, cache: false, // force reload dataType: "text" }).fail(function (jqXHR, textStatus) { var error = jqXHR.statusText === "OK" ? textStatus : jqXHR.statusText; reject(Error("GET <" + url + "> failed: " + error)); }).done(function (data) { try { _cache.meta.base = url; resolver._setBase(url); _cache.set(data, url); $("#loadForm").dialog("close"); toggleControls(); resolve({ url: url, data: data }); } catch (e) { reject(Error("unable to " + (e.action || "evaluate") + " <" + url + ">: " + '\n' + e.message)); } }); }); }, url: undefined // only set if inputarea caches some web resource. }; resolver = new IRIResolver(ret.meta); ret.meta.termToLex = function (trm) { return rdflib_termToLex(trm, resolver); }; ret.meta.lexToTerm = function (lex) { return rdflib_lexToTerm(lex, resolver); }; return ret; } function makeSchemaCache (selection) { var ret = _makeCache(selection); var graph = null; ret.language = null; ret.parse = function (text, base) { var isJSON = text.match(/^\s*\{/); graph = isJSON ? null : tryN3(text); this.language = isJSON ? "ShExJ" : graph ? "ShExR" : "ShExC"; $("#results .status").text("parsing "+this.language+" schema...").show(); var schema = isJSON ? ShEx.Util.ShExJtoAS(JSON.parse(text)) : graph ? parseShExR() : parseShEx(text, ret.meta, base); $("#results .status").hide(); return schema; function tryN3 (text) { try { if (text.match(/^\s*$/)) return null; var db = parseTurtle (text, ret.meta, DefaultBase); // interpret empty schema as ShExC if (db.getTriples().length === 0) return null; return db; } catch (e) { return null; } } function parseShExR () { var graphParser = ShEx.Validator.construct( parseShEx(ShExRSchema, {}, base), // !! do something useful with the meta parm (prefixes and base) {} ); var schemaRoot = graph.getTriples(null, ShEx.Util.RDF.type, "http://www.w3.org/ns/shex#Schema")[0].subject; var val = graphParser.validate(ShEx.Util.makeN3DB(graph), schemaRoot); // start shape return ShEx.Util.ShExJtoAS(ShEx.Util.ShExRtoShExJ(ShEx.Util.valuesToSchema(ShEx.Util.valToValues(val)))); } }; ret.getItems = function () { var obj = this.refresh(); var start = "start" in obj ? [START_SHAPE_LABEL] : []; var rest = "shapes" in obj ? Object.keys(obj.shapes).map(Caches.inputSchema.meta.termToLex) : []; return start.concat(rest); }; return ret; } function makeTurtleCache (selection) { var ret = _makeCache(selection); ret.parse = function (text, base) { return ShEx.Util.makeN3DB(parseTurtle(text, ret.meta, base)); }; ret.getItems = function () { var data = this.refresh(); return data.getTriplesByIRI().map(t => { return Caches.inputData.meta.termToLex(t.subject); }); }; return ret; } function makeManifestCache (selection) { var ret = _makeCache(selection); ret.set = function (textOrObj, url, source) { $("#inputSchema .manifest li").remove(); $("#inputData .passes li, #inputData .fails li").remove(); if (typeof textOrObj !== "object") { try { // exceptions pass through to caller (asyncGet) textOrObj = JSON.parse(textOrObj); } catch (e) { $("#inputSchema .manifest").append($("
  • ").text(NO_MANIFEST_LOADED)); var throwMe = Error(e + '\n' + textOrObj); throwMe.action = 'load manifest' throw throwMe // @@DELME(2017-12-29) // transform deprecated examples.js structure // textOrObj = eval(textOrObj).reduce(function (acc, schema) { // function x (data, status) { // return { // schemaLabel: schema.name, // schema: schema.schema, // dataLabel: data.name, // data: data.data, // queryMap: data.queryMap, // status: status // }; // } // return acc.concat( // schema.passes.map(data => x(data, "conformant")), // schema.fails.map(data => x(data, "nonconformant")) // ); // }, []); } } if (textOrObj.constructor !== Array) textOrObj = [textOrObj]; var demos = textOrObj.reduce((acc, elt) => { if ("action" in elt) { // compatibility with test suite structure. var action = elt.action; var schemaLabel = action.schemaURL.substr(action.schemaURL.lastIndexOf('/')+1); var dataLabel = elt["@id"]; var match = null; var emptyGraph = "-- empty graph --"; if ("comment" in elt) { if ((match = elt.comment.match(/^(.*?) \/ { (.*?) }$/))) { schemaLabel = match[1]; dataLabel = match[2] || emptyGraph; } else if ((match = elt.comment.match(/^(.*?) on { (.*?) }$/))) { schemaLabel = match[1]; dataLabel = match[2] || emptyGraph; } else if ((match = elt.comment.match(/^(.*?) as { (.*?) }$/))) { schemaLabel = match[2]; dataLabel = match[1] || emptyGraph; } } var queryMap = "map" in action ? null : ldToTurtle(action.focus, Caches.inputData.meta.termToLex) + "@" + ("shape" in action ? ldToTurtle(action.shape, Caches.inputSchema.meta.termToLex) : START_SHAPE_LABEL); var queryMapURL = "map" in action ? action.map : null; elt = Object.assign( { schemaLabel: schemaLabel, schema: action.schema, schemaURL: action.schemaURL || url, // dataLabel: "comment" in elt ? elt.comment : (queryMap || dataURL), dataLabel: dataLabel, data: action.data, dataURL: action.dataURL || DefaultBase }, (queryMap ? { queryMap: queryMap } : { queryMapURL: queryMapURL }), { status: elt["@type"] === "sht:ValidationFailure" ? "nonconformant" : "conformant" } ); if ("termResolver" in action || "termResolverURL" in action) { elt.meta = action.termResolver; elt.metaURL = action.termResolverURL || DefaultBase; } } ["schemaURL", "dataURL", "queryMapURL"].forEach(parm => { if (parm in elt) { elt[parm] = new URL(elt[parm], new URL(url, DefaultBase).href).href; } else { delete elt[parm]; } }); return acc.concat(elt); }, []); prepareManifest(demos, url); $("#manifestDrop").show(); // may have been hidden if no manifest loaded. }; ret.parse = function (text, base) { throw Error("should not try to parse manifest cache"); }; ret.getItems = function () { throw Error("should not try to get manifest cache items"); }; return ret; function maybeGET(obj, base, key, accept) { if (obj[key] != null) { // Take the passed data, guess base if not provided. if (!(key + "URL" in obj)) obj[key + "URL"] = base; obj[key] = Promise.resolve(obj[key]); } else if (key + "URL" in obj) { // absolutize the URL obj[key + "URL"] = ret.meta.lexToTerm("<"+obj[key + "URL"]+">"); // Load the remote resource. obj[key] = new Promise((resolve, reject) => { $.ajax({ accepts: { mycustomtype: accept }, url: ret.meta.lexToTerm("<"+obj[key + "URL"]+">"), dataType: "text" }).then(text => { resolve(text); }).fail(e => { results.append($("
    ").text(
                      "Error " + e.status + " " + e.statusText + " on GET " + obj[key + "URL"]
                    ).addClass("error"));
                    reject(e);
                  });
                });
              } else {
                // Ignore this parameter.
                obj[key] = Promise.resolve(obj[key]);
              }
            }
    }
    
    
            function ldToTurtle (ld, termToLex) {
              return typeof ld === "object" ? lit(ld) : termToLex(ld);
              function lit (o) {
                let ret = "\""+o["@value"].replace(/["\r\n\t]/g, (c) => {
                  return {'"': "\\\"", "\r": "\\r", "\n": "\\n", "\t": "\\t"}[c];
                }) +"\"";
                if ("@type" in o)
                  ret += "^^<" + o["@type"] + ">";
                if ("@language" in o)
                  ret += "@" + o["@language"];
                return ret;
              }
            }
    
    function makeShapeMapCache (selection) {
      var ret = _makeCache(selection);
      ret.parse = function (text) {
        removeEditMapPair(null);
        $("#textMap").val(text);
        copyTextMapToEditMap();
        copyEditMapToFixedMap();
      };
      // ret.parse = function (text, base) {  };
      ret.getItems = function () {
        throw Error("should not try to get manifest cache items");
      };
      return ret;
    }
    
    // controls for manifest buttons
    function paintManifest (selector, list, func, listItems, side) {
      $(selector).empty();
      list.forEach(entry => {
        var button = $("").
                   css("border-radius", ".5em").
                   on("click", function () {
                     Caches.inputSchema.set($("#results div").text(), DefaultBase);
                   })).
            append(":").
            show();
          var parsedSchema;
          if (Caches.inputSchema.language === "ShExJ") {
            new ShEx.Writer({simplifyParentheses: false}).writeSchema(Caches.inputSchema.parsed, (error, text) => {
              if (error) {
                $("#results .status").text("unwritable ShExJ schema:\n" + error).show();
                // res.addClass("error");
              } else {
                results.append($("
    ").text(text).addClass("passes"));
              }
            });
          } else {
            var pre = $("
    ");
            pre.text(JSON.stringify(ShEx.Util.AStoShExJ(ShEx.Util.canonicalize(Caches.inputSchema.parsed)), null, "  ")).addClass("passes");
            results.append(pre);
          }
          results.finish();
          if (done) { done() }
        }
    
        function noStack (f) {
          try {
            f();
          } catch (e) {
            // The Parser error stack is uninteresting.
            delete e.stack;
            throw e;
          }
        }
      } catch (e) {
        failMessage(e, currentAction);
        console.error(e); // dump details to console.
        if (done) { done(e) }
      }
    
      function makeConsoleTracker () {
        function padding (depth) { return (new Array(depth + 1)).join("  "); } // AKA "  ".repeat(depth)
        function sm (node, shape) {
          return `${Caches.inputData.meta.termToLex(node)}@${Caches.inputSchema.meta.termToLex(shape)}`;
        }
        var logger = {
          recurse: x => { console.log(`${padding(logger.depth)}↻ ${sm(x.node, x.shape)}`); return x; },
          known: x => { console.log(`${padding(logger.depth)}↵ ${sm(x.node, x.shape)}`); return x; },
          enter: (point, label) => { console.log(`${padding(logger.depth)}→ ${sm(point, label)}`); ++logger.depth; },
          exit: (point, label, ret) => { --logger.depth; console.log(`${padding(logger.depth)}← ${sm(point, label)}`); },
          depth: 0
        };
        return logger;
      }
    
      function renderEntry (entry) {
        var fails = entry.status === "nonconformant";
        var klass = fails ? "fails" : "passes";
        var resultStr = fails ? "✗" : "✓";
        var elt = null;
    
        switch ($("#interface").val()) {
        case "human":
          elt = $("
    ").append( $("").text(resultStr), $("").text( `${Caches.inputSchema.meta.termToLex(entry.node)}@${fails ? "!" : ""}${Caches.inputData.meta.termToLex(entry.shape)}` )).addClass(klass); if (fails) elt.append($("
    ").text(ShEx.Util.errsToSimple(entry.appinfo).join("\n")));
          break;
    
        case "minimal":
          if (fails)
            entry.reason = ShEx.Util.errsToSimple(entry.appinfo).join("\n");
          delete entry.appinfo;
          // fall through to default
        default:
          elt = $("
    ").text(JSON.stringify(entry, null, "  ")).addClass(klass);
        }
        results.append(elt);
    
        // update the FixedMap
        var shapeString = entry.shape === ShEx.Validator.start ? START_SHAPE_INDEX_ENTRY : entry.shape;
        var fixedMapEntry = $("#fixedMap .pair"+
                              "[data-node='"+entry.node+"']"+
                              "[data-shape='"+shapeString+"']");
        fixedMapEntry.addClass(klass).find("a").text(resultStr);
        var nodeLex = fixedMapEntry.find("input.focus").val();
        var shapeLex = fixedMapEntry.find("input.inputShape").val();
        var anchor = encodeURIComponent(nodeLex) + "@" + encodeURIComponent(shapeLex);
        elt.attr("id", anchor);
        fixedMapEntry.find("a").attr("href", "#" + anchor);
        fixedMapEntry.attr("title", entry.elapsed + " ms")
      }
    
      function finishRendering (done) {
              $("#results .status").text("rendering results...").show();
              // Add commas to JSON results.
              if ($("#interface").val() !== "human")
                $("#results div *").each((idx, elt) => {
                  if (idx === 0)
                    $(elt).prepend("[");
                  $(elt).append(idx === $("#results div *").length - 1 ? "]" : ",");
                });
          $("#results .status").hide();
          // for debugging values and schema formats:
          // try {
          //   var x = ShEx.Util.valToValues(ret);
          //   // var x = ShEx.Util.ShExJtoAS(valuesToSchema(valToValues(ret)));
          //   res = results.replace(JSON.stringify(x, null, "  "));
          //   var y = ShEx.Util.valuesToSchema(x);
          //   res = results.append(JSON.stringify(y, null, "  "));
          // } catch (e) {
          //   console.dir(e);
          // }
          results.finish();
      }
    }
    
    var LastFailTime = 0;
    function failMessage (e, action, text) {
      $("#results .status").empty().text("Errors encountered:").show()
      var div = $("
    ").addClass("error"); div.append($("

    ").text("error " + action + ":\n")); div.append($("
    ").text(e.message));
      if (text)
        div.append($("
    ").text(text));
      results.append(div);
      LastFailTime = new Date().getTime();
    }
    
    function addEmptyEditMapPair (evt) {
      addEditMapPairs(null, $(evt.target).parent().parent());
      markEditMapDirty();
      return false;
    }
    
    function addEditMapPairs (pairs, target) {
      (pairs || [{node: {type: "empty"}}]).forEach(pair => {
        var nodeType = (typeof pair.node !== "object" || "@value" in pair.node)
            ? "node"
            : pair.node.type;
        var skip = false;
        var node; var shape;
        switch (nodeType) {
        case "empty": node = shape = ""; break;
        case "node": node = ldToTurtle(pair.node, Caches.inputData.meta.termToLex); shape = startOrLdToTurtle(pair.shape); break;
        case "TriplePattern": node = renderTP(pair.node); shape = startOrLdToTurtle(pair.shape); break;
        case "Extension":
          failMessage(Error("unsupported extension: <" + pair.node.language + ">"),
                      "parsing Query Map", pair.node.lexical);
          skip = true; // skip this entry.
          break;
        default:
          results.append($("
    ").append( $("").text("unrecognized ShapeMap:"), $("
    ").text(JSON.stringify(pair))
          ).addClass("error"));
          skip = true; // skip this entry.
          break;
        }
        if (!skip) {
    
        var spanElt = $("", {class: "pair"});
        var focusElt = $("