esvit-node-schematron
Version:
A pure-JS Schematron implementation for Node, browsers and CLI
415 lines (332 loc) • 14.7 kB
JavaScript
import { evaluateXPathToString, evaluateXPathToBoolean, evaluateXPath, evaluateXPathToNodes } from 'fontoxpath';
export { registerCustomXPathFunction } from 'fontoxpath';
import fs from 'fs';
import { sync } from 'slimdom-sax-parser';
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _createForOfIteratorHelperLoose(o, allowArrayLike) {
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
if (it) return (it = it.call(o)).next.bind(it);
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
return function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var Namespace = /*#__PURE__*/function () {
function Namespace(prefix, uri) {
this.prefix = prefix;
this.uri = uri;
}
Namespace.fromJson = function fromJson(json) {
return new Namespace(json.prefix, json.uri);
};
return Namespace;
}();
Namespace.QUERY = "map {\n\t\t\"prefix\": @prefix/string(),\n\t\t\"uri\": @uri/string()\n\t}";
var Result = /*#__PURE__*/function () {
function Result( // pattern: Pattern,
// phase?: Phase,
// rule: Rule,
context, assert, message) {
// this.pattern = pattern;
// this.phase = phase;
// this.rule = rule;
this.assertId = assert.id;
this.isReport = assert.isReport;
this.context = context;
this.message = message;
}
var _proto = Result.prototype;
_proto.toJson = function toJson() {
return {
assertId: this.assertId,
isReport: this.isReport,
context: this.context.outerHTML,
message: this.message
};
};
return Result;
}();
var Assert = /*#__PURE__*/function () {
function Assert(id, test, message, isReport) {
this.id = id;
this.test = test;
this.message = message;
this.isReport = isReport;
}
var _proto = Assert.prototype;
_proto.createMessageString = function createMessageString(contextNode, variables, fontoxpathOptions, chunks) {
return chunks.map(function (chunk) {
if (typeof chunk === 'string') {
return chunk;
} // <sch:name />
if (chunk.$type === 'name') {
return evaluateXPathToString('name(' + (chunk.path || '') + ')', contextNode, null, variables, fontoxpathOptions);
} // <sch:value-of />
if (chunk.$type === 'value-of') {
return evaluateXPathToString(chunk.select, contextNode, null, variables, fontoxpathOptions);
}
console.log(chunk);
throw new Error('Unsupported element in <sch:message>');
}).join('');
};
_proto.validateNode = function validateNode(context, variables, fontoxpathOptions) {
var outcome = evaluateXPathToBoolean(this.test, context, null, variables, fontoxpathOptions);
return !this.isReport && outcome || this.isReport && !outcome ? null : new Result(context, this, this.createMessageString(context, variables, fontoxpathOptions, this.message));
};
Assert.fromJson = function fromJson(json) {
return new Assert(json.id, json.test, json.message, json.isReport);
};
return Assert;
}();
Assert.QUERY = "map {\n\t\t'id': if (@id) then string(@id) else (),\n\t\t'test': @test/string(),\n\t\t'message': array { (./text()|./element())/local:json(.) },\n\t\t'isReport': boolean(local-name() = 'report')\n\t}";
var Variable = /*#__PURE__*/function () {
function Variable(name, value) {
this.name = name.trim();
this.value = value;
}
Variable.reduceVariables = function reduceVariables(context, variables, fontoxpathOptions, initial) {
return variables.reduce(function (mapping, variable) {
var _Object$assign;
return Object.assign(mapping, (_Object$assign = {}, _Object$assign[variable.name] = variable.value ? evaluateXPath(variable.value, context, null, mapping, undefined, fontoxpathOptions) : context, _Object$assign));
}, initial || {});
};
Variable.fromJson = function fromJson(json) {
return new Variable(json.name, json.value);
};
return Variable;
}();
Variable.QUERY = "map {\n\t\t'name': @name/string(),\n\t\t'value': @value/string()\n\t}";
var Rule = /*#__PURE__*/function () {
function Rule(context, variables, asserts) {
this.context = context;
this.variables = variables;
this.asserts = asserts;
}
var _proto = Rule.prototype;
_proto.validateNode = function validateNode(context, parentVariables, fontoxpathOptions) {
var variables = Variable.reduceVariables(context, this.variables, fontoxpathOptions, _extends({}, parentVariables));
return this.asserts.map(function (assert) {
return assert.validateNode(context, variables, fontoxpathOptions);
}).filter(function (result) {
return result !== null;
});
};
Rule.fromJson = function fromJson(json) {
var variables = json.variables.map(function (rule) {
return Variable.fromJson(rule);
});
var asserts = json.asserts.map(function (rule) {
return Assert.fromJson(rule);
});
return new Rule(json.context.trim(), variables, asserts);
};
return Rule;
}();
Rule.QUERY = "map {\n\t\t'context': @context/string(),\n\t\t'variables': array { ./(sch:let|sch:param)/" + Variable.QUERY + "},\n\t\t'asserts': array{ ./(sch:report|sch:assert)/" + Assert.QUERY + "}\n\t}";
var Pattern = /*#__PURE__*/function () {
function Pattern(id, rules, variables) {
this.id = id;
this.rules = rules;
this.variables = variables;
}
var _proto = Pattern.prototype;
_proto.validateDocument = function validateDocument(documentDom, parentVariables, fontoxpathOptions) {
var _this = this;
var variables = Variable.reduceVariables(documentDom, this.variables, fontoxpathOptions, _extends({}, parentVariables));
var ruleContexts = this.rules.map(function (rule) {
return evaluateXPathToNodes('//(' + rule.context + ')', documentDom, null, variables, fontoxpathOptions);
});
var flattenValidationResults = function flattenValidationResults(results, node) {
var ruleIndex = ruleContexts.findIndex(function (context) {
return context.includes(node);
});
var rule = ruleIndex >= 0 ? _this.rules[ruleIndex] : null;
if (rule) {
results.splice.apply(results, [results.length, 0].concat(rule.validateNode(node, variables, fontoxpathOptions)));
}
return Array.from(node.childNodes).reduce(flattenValidationResults, results);
};
return Array.from(documentDom.childNodes).reduce(flattenValidationResults, []);
};
Pattern.fromJson = function fromJson(json) {
return new Pattern(json.id, json.rules.map(function (obj) {
return Rule.fromJson(obj);
}), json.variables.map(function (obj) {
return Variable.fromJson(obj);
}));
};
return Pattern;
}();
Pattern.QUERY = "map {\n\t\t'id': @id/string(),\n\t\t'rules': array{ ./sch:rule/" + Rule.QUERY + "},\n\t\t'variables': array { ./sch:let/" + Variable.QUERY + "}\n\t}";
var Phase = /*#__PURE__*/function () {
function Phase(id, active, variables) {
this.id = id;
this.active = active;
this.variables = variables;
}
Phase.fromJson = function fromJson(json) {
return new Phase(json.id, json.active, json.variables.map(function (rule) {
return Variable.fromJson(rule);
}));
};
return Phase;
}();
Phase.QUERY = "map {\n\t\t\"id\": @id/string(),\n\t\t\"active\": array { ./sch:active/@pattern/string() },\n\t\t'variables': array { ./sch:let/" + Variable.QUERY + "}\n\t}";
var Schema = /*#__PURE__*/function () {
function Schema(title, defaultPhase, variables, phases, patterns, namespaces) {
this.title = title;
this.defaultPhase = defaultPhase;
this.variables = variables;
this.phases = phases;
this.patterns = patterns;
this.namespaces = namespaces;
}
var _proto = Schema.prototype;
_proto.validateString = function validateString(documentXmlString, options) {
// Typescript casting slimdom.Document to Document, which are the same
return this.validateDocument(sync(documentXmlString), options);
};
_proto.validateDocument = function validateDocument(documentDom, options) {
var _this = this;
var _ref = options || {},
phaseId = _ref.phaseId,
debug = _ref.debug;
if (!phaseId) {
phaseId = '#DEFAULT';
}
if (phaseId === '#DEFAULT') {
phaseId = this.defaultPhase || '#ALL';
}
var fontoxpathOptions = {
namespaceResolver: this.getNamespaceUriForPrefix.bind(this),
debug: debug
};
var variables = Variable.reduceVariables(documentDom, this.variables, fontoxpathOptions, {});
if (phaseId === '#ALL') {
return this.patterns.reduce(function (results, pattern) {
return results.concat(pattern.validateDocument(documentDom, variables, fontoxpathOptions));
}, []);
}
var phase = this.phases.find(function (phase) {
return phase.id === phaseId;
});
var phaseVariables = Variable.reduceVariables(documentDom, (phase == null ? void 0 : phase.variables) || [], fontoxpathOptions, _extends({}, variables));
return (phase == null ? void 0 : phase.active.map(function (patternId) {
return _this.patterns.find(function (pattern) {
return pattern.id === patternId;
});
}).reduce(function (results, pattern) {
return results.concat((pattern == null ? void 0 : pattern.validateDocument(documentDom, phaseVariables, fontoxpathOptions)) || []);
}, [])) || [];
} // TODO more optimally store the namespace prefix/uri mapping. Right now its modeled as an array because there
// is a list of <ns> elements that are not really guaranteed to use unique prefixes.
;
_proto.getNamespaceUriForPrefix = function getNamespaceUriForPrefix(prefix) {
if (!prefix) {
return null;
}
var ns = this.namespaces.find(function (ns) {
return ns.prefix === prefix;
});
if (!ns) {
throw new Error("Namespace prefix \"" + prefix + "\" could not be resolved to an URI using <sch:ns>");
}
return ns.uri;
};
Schema.getElementsByTagName = function getElementsByTagName(node, tagName) {
var elements = []; // Helper function to recursively traverse the DOM tree
function traverse(currentNode) {
if (currentNode.nodeType === 1 && currentNode.tagName === tagName) {
elements.push(currentNode);
}
for (var _iterator = _createForOfIteratorHelperLoose(currentNode.childNodes), _step; !(_step = _iterator()).done;) {
var child = _step.value;
traverse(child);
}
}
traverse(node); // Start traversing from the given node
return elements;
};
Schema.processIncludes = function processIncludes(schematronDom, options) {
if (options === void 0) {
options = {};
}
var includeElements = this.getElementsByTagName(schematronDom, 'include');
for (var i = 0; i < includeElements.length; i++) {
var includeElement = includeElements[i];
var href = includeElement.getAttribute('href');
if (href) {
var _includeElement$paren, _includeElement$paren2;
var includedContent = fs.readFileSync(options.resourceDir + "/" + href, 'utf-8');
var includedDom = sync(includedContent);
Schema.processIncludes(includedDom);
(_includeElement$paren = includeElement.parentNode) == null ? void 0 : _includeElement$paren.insertBefore(includedDom.documentElement, includeElement);
(_includeElement$paren2 = includeElement.parentNode) == null ? void 0 : _includeElement$paren2.removeChild(includeElement);
}
}
};
Schema.fromJson = function fromJson(json) {
return new Schema(json.title, json.defaultPhase, json.variables.map(function (obj) {
return Variable.fromJson(obj);
}), json.phases.map(function (obj) {
return Phase.fromJson(obj);
}), json.patterns.map(function (obj) {
return Pattern.fromJson(obj);
}), json.namespaces.map(function (obj) {
return Namespace.fromJson(obj);
}));
};
Schema.fromDomToJson = function fromDomToJson(schematronDom) {
return evaluateXPath(Schema.QUERY, schematronDom, null, {}, undefined, {
language: evaluateXPath.XQUERY_3_1_LANGUAGE
});
};
Schema.fromDom = function fromDom(schematronDom) {
return Schema.fromJson(Schema.fromDomToJson(schematronDom));
};
Schema.fromString = function fromString(schematronXmlString, options) {
var schematronDom = sync(schematronXmlString);
Schema.processIncludes(schematronDom, options);
return Schema.fromDom(schematronDom);
};
return Schema;
}();
Schema.QUERY = "\n\t\tdeclare namespace sch = 'http://purl.oclc.org/dsdl/schematron';\n\n\t\tdeclare function local:json($node as node()) {\n\t\t\tif ($node[self::text()])\n\t\t\t\tthen $node/string()\n\t\t\telse\n\t\t\tmap:merge((\n\t\t\t\tmap:entry('$type', $node/local-name()),\n\t\t\t\tfor $attr in $node/@*\n\t\t\t\t\treturn map:entry($attr/name(), $attr/string())\n\t\t\t))\n\t\t};\n\n\t\tlet $context := /*[1]\n\t\treturn map {\n\t\t\t'title': $context/@title/string(),\n\t\t\t'defaultPhase': $context/@defaultPhase/string(),\n\t\t\t'phases': array { $context/sch:phase/" + Phase.QUERY + "},\n\t\t\t'patterns': array { $context/sch:pattern/" + Pattern.QUERY + "},\n\t\t\t'variables': array { $context/sch:let/" + Variable.QUERY + "},\n\t\t\t'namespaces': array { $context/sch:ns/" + Namespace.QUERY + "}\n\t\t}\n\t";
export { Schema };
//# sourceMappingURL=esvit-node-schematron.esm.js.map