jats-xml 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/cjs/cli/index.js +2 -0
- package/dist/cjs/cli/jats-test.js +125 -0
- package/dist/cjs/cli/parse.js +2 -1
- package/dist/cjs/cli/validate.js +11 -4
- package/dist/cjs/utils.js +7 -2
- package/dist/cjs/validate/dtd.js +168 -18
- package/dist/cjs/version.js +1 -1
- package/dist/esm/cli/index.js +2 -0
- package/dist/esm/cli/jats-test.js +117 -0
- package/dist/esm/cli/parse.js +1 -1
- package/dist/esm/cli/validate.js +11 -4
- package/dist/esm/utils.js +7 -2
- package/dist/esm/validate/dtd.js +166 -17
- package/dist/esm/version.js +1 -1
- package/dist/jats.js +3571 -608
- package/dist/types/cli/jats-test.d.ts +9 -0
- package/dist/types/cli/jats-test.d.ts.map +1 -0
- package/dist/types/cli/parse.d.ts +3 -0
- package/dist/types/cli/parse.d.ts.map +1 -1
- package/dist/types/cli/validate.d.ts.map +1 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/types/validate/dtd.d.ts +19 -2
- package/dist/types/validate/dtd.d.ts.map +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -53,6 +53,28 @@ This will provide a summary, including a list of what the JATS file contains.
|
|
|
53
53
|
jats validate article.jats --jats 1.2 --mathmml 2
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
`test`: test a JATS file against a list of unit tests in YAML
|
|
57
|
+
|
|
58
|
+
The test cases are useful for known exports and expecting specific pieces of information in the XML.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
jats test article.jats --cases tests.yml
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
cases:
|
|
66
|
+
- title: Correct publisher ID (publisher-id)
|
|
67
|
+
select: 'front > journal-meta > journal-id[journal-id-type="publisher-id"] > *'
|
|
68
|
+
equals:
|
|
69
|
+
type: text
|
|
70
|
+
value: plos
|
|
71
|
+
- title: Every orcid is authenticated
|
|
72
|
+
selectAll: 'front > article-meta > contrib-group > contrib > contrib-id'
|
|
73
|
+
equals:
|
|
74
|
+
contrib-id-type: orcid
|
|
75
|
+
authenticated: 'true'
|
|
76
|
+
```
|
|
77
|
+
|
|
56
78
|
## Working in Typescript
|
|
57
79
|
|
|
58
80
|
All tags are accessible as types/enums. There is also documentation from each node-type
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -8,9 +8,11 @@ const commander_1 = __importDefault(require("commander"));
|
|
|
8
8
|
const version_1 = __importDefault(require("../version"));
|
|
9
9
|
const parse_1 = require("./parse");
|
|
10
10
|
const validate_1 = require("./validate");
|
|
11
|
+
const jats_test_1 = require("./jats-test");
|
|
11
12
|
const program = new commander_1.default.Command();
|
|
12
13
|
(0, parse_1.addDownloadCLI)(program);
|
|
13
14
|
(0, validate_1.addValidateCLI)(program);
|
|
15
|
+
(0, jats_test_1.addTestCLI)(program);
|
|
14
16
|
program.version(`v${version_1.default}`, '-v, --version', 'Print the current version of jats-xml');
|
|
15
17
|
program.option('-d, --debug', 'Log out any errors to the console.');
|
|
16
18
|
program.parse(process.argv);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.addTestCLI = exports.testJatsFile = void 0;
|
|
16
|
+
const commander_1 = require("commander");
|
|
17
|
+
const myst_cli_utils_1 = require("myst-cli-utils");
|
|
18
|
+
const session_1 = require("../session");
|
|
19
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
21
|
+
const parse_1 = require("./parse");
|
|
22
|
+
const unist_util_select_1 = require("unist-util-select");
|
|
23
|
+
const unist_util_is_1 = require("unist-util-is");
|
|
24
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
25
|
+
const INDENT = ' ';
|
|
26
|
+
function printNodes(expected, received) {
|
|
27
|
+
return chalk_1.default.reset(`\n${INDENT}${chalk_1.default.greenBright('Expected node containing')}:\n${INDENT} ${js_yaml_1.default
|
|
28
|
+
.dump(expected)
|
|
29
|
+
.replace(/\n/g, `\n${INDENT} `)}\n${INDENT}${chalk_1.default.redBright('Received node')}:\n${INDENT} ${js_yaml_1.default.dump(received).replace(/\n/g, `\n${INDENT} `)}`);
|
|
30
|
+
}
|
|
31
|
+
function testJatsFile(session, file, opts) {
|
|
32
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
33
|
+
const toc = (0, myst_cli_utils_1.tic)();
|
|
34
|
+
const jats = yield (0, parse_1.parseJats)(session, file);
|
|
35
|
+
const tests = js_yaml_1.default.load(fs_1.default.readFileSync(opts.cases).toString());
|
|
36
|
+
const results = tests.cases.map((testCase, index) => {
|
|
37
|
+
if (!testCase.title) {
|
|
38
|
+
return [`Test Case ${index}`, null, 'Test must include a title'];
|
|
39
|
+
}
|
|
40
|
+
if (testCase.equals === undefined) {
|
|
41
|
+
return [testCase.title, null, 'Test must have an equals statement'];
|
|
42
|
+
}
|
|
43
|
+
if (testCase.select) {
|
|
44
|
+
const node = (0, unist_util_select_1.select)(testCase.select, jats.tree);
|
|
45
|
+
const pass = (0, unist_util_is_1.is)(node, testCase.equals);
|
|
46
|
+
if (testCase.equals == null && node) {
|
|
47
|
+
return [testCase.title, false, 'Expected no node to be present'];
|
|
48
|
+
}
|
|
49
|
+
if (!node && testCase.equals == null)
|
|
50
|
+
return [testCase.title, true];
|
|
51
|
+
if (!node)
|
|
52
|
+
return [testCase.title, false];
|
|
53
|
+
let failed = false;
|
|
54
|
+
const messages = [];
|
|
55
|
+
if (!pass) {
|
|
56
|
+
failed = failed || true;
|
|
57
|
+
messages.push(`Failed to validate node\n${printNodes(testCase.equals, node)}`);
|
|
58
|
+
}
|
|
59
|
+
return [testCase.title, !failed, messages.join('\n')];
|
|
60
|
+
}
|
|
61
|
+
else if (testCase.selectAll) {
|
|
62
|
+
const testNodes = (0, unist_util_select_1.selectAll)(testCase.selectAll, jats.tree);
|
|
63
|
+
if (!testNodes && testCase.equals == null)
|
|
64
|
+
return [testCase.title, true];
|
|
65
|
+
if (!testNodes)
|
|
66
|
+
return [testCase.title, false, 'Node not found'];
|
|
67
|
+
let equals = testCase.equals;
|
|
68
|
+
if (!Array.isArray(testCase.equals)) {
|
|
69
|
+
equals = Array(testNodes.length).fill(testCase.equals);
|
|
70
|
+
}
|
|
71
|
+
let failed = false;
|
|
72
|
+
const messages = [];
|
|
73
|
+
if (equals.length !== testNodes.length) {
|
|
74
|
+
failed = failed || true;
|
|
75
|
+
messages.push(`Expected ${equals.length} nodes, got ${testNodes.length}\n${printNodes(equals, testNodes)}`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
equals.forEach((node, ii) => {
|
|
79
|
+
const pass = (0, unist_util_is_1.is)(testNodes[ii], node);
|
|
80
|
+
if (!pass) {
|
|
81
|
+
failed = failed || true;
|
|
82
|
+
messages.push(`Failed to validate node ${ii}\n${printNodes(node, testNodes[ii])}`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return [testCase.title, !failed, messages.join('\n')];
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
return [testCase.title, false, 'Test must have either `select` or `selectAll`'];
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
results.forEach((result) => {
|
|
93
|
+
const [title, pass, message] = result;
|
|
94
|
+
if (pass === null)
|
|
95
|
+
session.log.info(`${chalk_1.default.redBright.bold(`ERROR`)} - ${title}\n ${chalk_1.default.blueBright(message)}`);
|
|
96
|
+
else if (pass)
|
|
97
|
+
session.log.info(`${chalk_1.default.green(`PASS`)} - ${title}`);
|
|
98
|
+
else
|
|
99
|
+
session.log.info(`${chalk_1.default.red(`FAIL`)} - ${title}\n\n${INDENT}${chalk_1.default.blueBright(message)}\n`);
|
|
100
|
+
}, true);
|
|
101
|
+
const passed = results.reduce((num, [, pass]) => num + (pass ? 1 : 0), 0);
|
|
102
|
+
const failed = results.length - passed;
|
|
103
|
+
if (failed > 0 && passed === 0) {
|
|
104
|
+
throw new Error(toc(`${chalk_1.default.red(`Failed ${failed} tests in %s`)} 👎`));
|
|
105
|
+
}
|
|
106
|
+
if (failed > 0) {
|
|
107
|
+
throw new Error(toc(`${chalk_1.default.green(`Passed ${passed}/${results.length} tests in %s`)}\n${chalk_1.default.red(`Failed ${failed} tests`)} 👎`));
|
|
108
|
+
}
|
|
109
|
+
session.log.info(chalk_1.default.green(toc(`Passed ${passed} tests in %s 🚀`)));
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
exports.testJatsFile = testJatsFile;
|
|
114
|
+
function makeTestCLI(program) {
|
|
115
|
+
const command = new commander_1.Command('test')
|
|
116
|
+
.description('Test JATS file against a list of cases')
|
|
117
|
+
.argument('<file>', 'JATS file to test')
|
|
118
|
+
.addOption(new commander_1.Option('--cases <value>', 'The YAML file of unit tests to test against'))
|
|
119
|
+
.action((0, myst_cli_utils_1.clirun)(testJatsFile, { program, getSession: session_1.getSession }));
|
|
120
|
+
return command;
|
|
121
|
+
}
|
|
122
|
+
function addTestCLI(program) {
|
|
123
|
+
program.addCommand(makeTestCLI(program));
|
|
124
|
+
}
|
|
125
|
+
exports.addTestCLI = addTestCLI;
|
package/dist/cjs/cli/parse.js
CHANGED
|
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.addDownloadCLI = void 0;
|
|
15
|
+
exports.addDownloadCLI = exports.parseJats = void 0;
|
|
16
16
|
const commander_1 = require("commander");
|
|
17
17
|
const fs_1 = __importDefault(require("fs"));
|
|
18
18
|
const path_1 = require("path");
|
|
@@ -77,6 +77,7 @@ function parseJats(session, file, opts = { resolvers: resolvers_1.DEFAULT_RESOLV
|
|
|
77
77
|
return jats;
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
exports.parseJats = parseJats;
|
|
80
81
|
function formatLongString(data, offset = 0, length = 88 - offset) {
|
|
81
82
|
const out = [data.slice(0, length)];
|
|
82
83
|
let left = data.slice(length);
|
package/dist/cjs/cli/validate.js
CHANGED
|
@@ -7,11 +7,18 @@ const session_1 = require("../session");
|
|
|
7
7
|
const validate_1 = require("../validate");
|
|
8
8
|
function makeValidateCLI(program) {
|
|
9
9
|
const command = new commander_1.Command('validate')
|
|
10
|
-
.description(
|
|
10
|
+
.description(`
|
|
11
|
+
Validate JATS file against DTD schema.
|
|
12
|
+
|
|
13
|
+
The JATS DTD schema file is fetched from nih.gov ftp server if not available locally.
|
|
14
|
+
This will attempt to infer the specific JATS DTD version, library, etc from the file header,
|
|
15
|
+
but options are available to override the inferred values.
|
|
16
|
+
`)
|
|
11
17
|
.argument('<file>', 'JATS file to validate')
|
|
12
|
-
.addOption(new commander_1.Option('--
|
|
13
|
-
.addOption(new commander_1.Option('--
|
|
14
|
-
.addOption(new commander_1.Option('--
|
|
18
|
+
.addOption(new commander_1.Option('--library <value>', 'JATS library - archiving, publishing, or authoring (default: archiving, if value cannot be inferred from file)'))
|
|
19
|
+
.addOption(new commander_1.Option('--jats <version>', 'JATS version, must be 1.1 or later (default: 1.3, if value cannot be inferred from file)'))
|
|
20
|
+
.addOption(new commander_1.Option('--mathml <version>', 'MathML version, 2 or 3 (default: 3, if value cannot be inferred from file)'))
|
|
21
|
+
.addOption(new commander_1.Option('--oasis', 'Use OASIS table model (default: false, if value cannot be inferred from file)'))
|
|
15
22
|
.addOption(new commander_1.Option('--directory <value>', 'Directory to save DTD file'))
|
|
16
23
|
.action((0, myst_cli_utils_1.clirun)(validate_1.validateJatsAgainstDtdWrapper, { program, getSession: session_1.getSession }));
|
|
17
24
|
return command;
|
package/dist/cjs/utils.js
CHANGED
|
@@ -14,13 +14,18 @@ function convertToUnist(node) {
|
|
|
14
14
|
const { name, attributes, elements } = node;
|
|
15
15
|
const children = elements === null || elements === void 0 ? void 0 : elements.map(convertToUnist).filter((n) => !!n);
|
|
16
16
|
const next = Object.assign({ type: name !== null && name !== void 0 ? name : 'unknown' }, attributes);
|
|
17
|
-
if (
|
|
17
|
+
if (name === 'code') {
|
|
18
|
+
next.value = elements === null || elements === void 0 ? void 0 : elements[0].text;
|
|
19
|
+
}
|
|
20
|
+
else if (children)
|
|
18
21
|
next.children = children;
|
|
19
22
|
return next;
|
|
20
23
|
}
|
|
21
24
|
case 'text': {
|
|
22
25
|
const { attributes, text } = node;
|
|
23
|
-
return Object.assign(Object.assign({ type: 'text' }, attributes), { value: String(text)
|
|
26
|
+
return Object.assign(Object.assign({ type: 'text' }, attributes), { value: String(text)
|
|
27
|
+
.replace(/\n(\s+)$/, '')
|
|
28
|
+
.replace('\n', ' ') });
|
|
24
29
|
}
|
|
25
30
|
case 'cdata': {
|
|
26
31
|
const { attributes, cdata } = node;
|
package/dist/cjs/validate/dtd.js
CHANGED
|
@@ -35,7 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
35
35
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
36
|
};
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.validateJatsAgainstDtdWrapper = exports.validateJatsAgainstDtd = void 0;
|
|
38
|
+
exports.validateJatsAgainstDtdWrapper = exports.validateJatsAgainstDtd = exports.inferOptions = void 0;
|
|
39
39
|
const fs_1 = __importStar(require("fs"));
|
|
40
40
|
const path_1 = __importDefault(require("path"));
|
|
41
41
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
@@ -43,15 +43,54 @@ const unzipper_1 = __importDefault(require("unzipper"));
|
|
|
43
43
|
const which_1 = require("which");
|
|
44
44
|
const myst_cli_utils_1 = require("myst-cli-utils");
|
|
45
45
|
const chalk_1 = __importDefault(require("chalk"));
|
|
46
|
-
const JATS_VERSIONS = [
|
|
46
|
+
const JATS_VERSIONS = [
|
|
47
|
+
'1.1',
|
|
48
|
+
'1.1d1',
|
|
49
|
+
'1.1d2',
|
|
50
|
+
'1.1d3',
|
|
51
|
+
'1.2',
|
|
52
|
+
'1.2d1',
|
|
53
|
+
'1.2d2',
|
|
54
|
+
'1.3',
|
|
55
|
+
'1.3d1',
|
|
56
|
+
'1.3d2',
|
|
57
|
+
];
|
|
47
58
|
const DEFAULT_JATS_VERSION = '1.3';
|
|
48
59
|
const MATHML_VERSIONS = ['2', '3'];
|
|
49
60
|
const DEFAULT_MATHML_VERSION = '3';
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
const JATS_LIBRARIES = ['authoring', 'publishing', 'archiving'];
|
|
62
|
+
const DEFAULT_JATS_LIBRARY = 'archiving';
|
|
63
|
+
/**
|
|
64
|
+
* Return static/ directory adjacent to the code
|
|
65
|
+
*
|
|
66
|
+
* This provides a standard location to cache DTD files, minimizing re-downloading.
|
|
67
|
+
*/
|
|
68
|
+
function defaultDirectory() {
|
|
69
|
+
return path_1.default.join(__dirname, 'static');
|
|
70
|
+
}
|
|
71
|
+
function warnOnOptionsMismatch(session, opts, inferredOpts) {
|
|
72
|
+
if (opts.jats && inferredOpts.jats && opts.jats !== inferredOpts.jats) {
|
|
73
|
+
session.log.warn(`Using JATS version ${opts.jats}; does not match version inferred from file ${inferredOpts.jats}`);
|
|
74
|
+
}
|
|
75
|
+
if (opts.library && inferredOpts.library && opts.library !== inferredOpts.library) {
|
|
76
|
+
session.log.warn(`Using JATS library ${opts.library}; does not match library inferred from file ${inferredOpts.library}`);
|
|
77
|
+
}
|
|
78
|
+
if (opts.mathml && inferredOpts.mathml && opts.mathml !== inferredOpts.mathml) {
|
|
79
|
+
session.log.warn(`Using MathML version ${opts.mathml}; does not match version inferred from file ${inferredOpts.mathml}`);
|
|
80
|
+
}
|
|
81
|
+
if (opts.oasis && !inferredOpts.oasis) {
|
|
82
|
+
session.log.warn('Using OASIS table model; does not match non-OASIS inferred from file');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Validate input value as JATS options and fill in defaults
|
|
87
|
+
*/
|
|
88
|
+
function validateOptions(session, opts, inferredOpts) {
|
|
89
|
+
var _a, _b, _c, _d, _e;
|
|
90
|
+
warnOnOptionsMismatch(session, opts, inferredOpts);
|
|
52
91
|
let jats;
|
|
53
92
|
if (!opts.jats) {
|
|
54
|
-
jats = DEFAULT_JATS_VERSION;
|
|
93
|
+
jats = (_a = inferredOpts.jats) !== null && _a !== void 0 ? _a : DEFAULT_JATS_VERSION;
|
|
55
94
|
}
|
|
56
95
|
else if (!JATS_VERSIONS.includes(opts.jats)) {
|
|
57
96
|
throw new Error(`Invalid JATS version "${opts.jats}" - must be one of [${JATS_VERSIONS.join(', ')}]`);
|
|
@@ -61,7 +100,7 @@ function validateOptions(opts) {
|
|
|
61
100
|
}
|
|
62
101
|
let mathml;
|
|
63
102
|
if (!opts.mathml) {
|
|
64
|
-
mathml = DEFAULT_MATHML_VERSION;
|
|
103
|
+
mathml = (_b = inferredOpts.mathml) !== null && _b !== void 0 ? _b : DEFAULT_MATHML_VERSION;
|
|
65
104
|
}
|
|
66
105
|
else if (!MATHML_VERSIONS.includes(opts.mathml)) {
|
|
67
106
|
throw new Error(`Invalid MathML version "${opts.mathml}" - must be one of [${MATHML_VERSIONS.join(', ')}]`);
|
|
@@ -69,41 +108,128 @@ function validateOptions(opts) {
|
|
|
69
108
|
else {
|
|
70
109
|
mathml = opts.mathml;
|
|
71
110
|
}
|
|
111
|
+
let library;
|
|
112
|
+
if (!opts.library) {
|
|
113
|
+
library = (_c = inferredOpts.library) !== null && _c !== void 0 ? _c : DEFAULT_JATS_LIBRARY;
|
|
114
|
+
}
|
|
115
|
+
else if (typeof opts.library !== 'string' ||
|
|
116
|
+
!JATS_LIBRARIES.includes(opts.library.toLowerCase())) {
|
|
117
|
+
throw new Error(`Invalid JATS library "${opts.library}" - must be one of [${JATS_LIBRARIES.join(', ')}]`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
library = opts.library.toLowerCase();
|
|
121
|
+
}
|
|
122
|
+
const oasis = (_d = inferredOpts.oasis) !== null && _d !== void 0 ? _d : !!opts.oasis;
|
|
123
|
+
if (library === 'authoring' && oasis) {
|
|
124
|
+
throw new Error('JATS article authoring library cannot use OASIS table model');
|
|
125
|
+
}
|
|
72
126
|
const out = {
|
|
127
|
+
library,
|
|
73
128
|
jats,
|
|
74
129
|
mathml,
|
|
75
|
-
oasis
|
|
76
|
-
directory: (
|
|
130
|
+
oasis,
|
|
131
|
+
directory: (_e = opts.directory) !== null && _e !== void 0 ? _e : defaultDirectory(),
|
|
77
132
|
};
|
|
78
133
|
return out;
|
|
79
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* DTD folder name
|
|
137
|
+
*/
|
|
80
138
|
function dtdFolder(opts) {
|
|
81
139
|
const version = opts.jats.replace('.', '-');
|
|
82
140
|
const oasis = opts.oasis ? '-OASIS' : '';
|
|
83
141
|
const mathml = `MathML${opts.mathml}`;
|
|
84
|
-
|
|
142
|
+
const library = opts.library.charAt(0).toUpperCase() + opts.library.slice(1);
|
|
143
|
+
return `JATS-${library}-${version}${oasis}-${mathml}-DTD`;
|
|
85
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* DTD zip file name on FTP server
|
|
147
|
+
*/
|
|
86
148
|
function dtdZipFile(opts) {
|
|
87
149
|
return `${dtdFolder(opts)}.zip`;
|
|
88
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Local location of DTD zip file
|
|
153
|
+
*/
|
|
89
154
|
function localDtdZipFile(opts) {
|
|
90
155
|
return path_1.default.join(opts.directory, dtdZipFile(opts));
|
|
91
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Extracted DTD file name
|
|
159
|
+
*/
|
|
92
160
|
function dtdFile(opts) {
|
|
93
|
-
const version = opts.jats.startsWith('1.
|
|
94
|
-
|
|
161
|
+
const version = opts.jats.startsWith('1.3') ? opts.jats.replace('.', '-') : '1';
|
|
162
|
+
let article;
|
|
163
|
+
if (opts.library === 'archiving') {
|
|
164
|
+
article = opts.oasis ? 'archive-oasis-article' : 'archivearticle';
|
|
165
|
+
}
|
|
166
|
+
else if (opts.library === 'publishing') {
|
|
167
|
+
article = opts.oasis ? 'journalpublishing-oasis-article' : 'journalpublishing';
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
article = 'articleauthoring';
|
|
171
|
+
}
|
|
95
172
|
const mathml = opts.mathml === '3' ? '-mathml3' : '';
|
|
96
173
|
return `JATS-${article}${version}${mathml}.dtd`;
|
|
97
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Local location of extracted DTD file
|
|
177
|
+
*/
|
|
98
178
|
function localDtdFile(opts) {
|
|
99
179
|
return path_1.default.join(opts.directory, dtdFolder(opts), dtdFile(opts));
|
|
100
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* NIH FTP server and path for downloading JATS DTD files
|
|
183
|
+
*
|
|
184
|
+
* This is accessed by node-fetch over https.
|
|
185
|
+
*/
|
|
101
186
|
function ftpUrl(opts) {
|
|
102
|
-
|
|
187
|
+
const library = opts.library === 'authoring' ? 'articleauthoring' : opts.library;
|
|
188
|
+
return `https://ftp.ncbi.nih.gov/pub/jats/${library}/${opts.jats}/${dtdZipFile(opts)}`;
|
|
103
189
|
}
|
|
104
|
-
|
|
105
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Create a DTS-filename-options lookup for implicitly setting options based on JATS header content
|
|
192
|
+
*/
|
|
193
|
+
function buildDtdFileLookup() {
|
|
194
|
+
const lookup = {};
|
|
195
|
+
JATS_VERSIONS.filter((jats) => jats === '1.2' || jats.startsWith('1.3')).forEach((jats) => {
|
|
196
|
+
MATHML_VERSIONS.forEach((mathml) => {
|
|
197
|
+
JATS_LIBRARIES.forEach((library) => {
|
|
198
|
+
(library === 'authoring' ? [false] : [true, false]).forEach((oasis) => {
|
|
199
|
+
const opts = { jats, mathml, library, oasis };
|
|
200
|
+
lookup[dtdFile(opts)] = opts;
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
return lookup;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Infer DTD options from file content
|
|
209
|
+
*
|
|
210
|
+
* This looks at DTD file name in DOCTYPE as well as dtd-version in article element
|
|
211
|
+
*/
|
|
212
|
+
function inferOptions(file) {
|
|
213
|
+
var _a, _b;
|
|
214
|
+
const data = fs_1.default.readFileSync(file).toString();
|
|
215
|
+
const doctype = (_a = data.match(/<!DOCTYPE [\s\S]+?">/g)) === null || _a === void 0 ? void 0 : _a[0];
|
|
216
|
+
const lookup = buildDtdFileLookup();
|
|
217
|
+
let opts = {};
|
|
218
|
+
Object.entries(lookup).forEach(([key, value]) => {
|
|
219
|
+
if (doctype === null || doctype === void 0 ? void 0 : doctype.includes(key))
|
|
220
|
+
opts = Object.assign({}, value);
|
|
221
|
+
});
|
|
222
|
+
const article = (_b = data.match(/<article [\s\S]+?>/g)) === null || _b === void 0 ? void 0 : _b[0];
|
|
223
|
+
JATS_VERSIONS.forEach((jats) => {
|
|
224
|
+
if (article === null || article === void 0 ? void 0 : article.includes(`dtd-version="${jats}"`))
|
|
225
|
+
opts.jats = jats;
|
|
226
|
+
});
|
|
227
|
+
return opts;
|
|
106
228
|
}
|
|
229
|
+
exports.inferOptions = inferOptions;
|
|
230
|
+
/**
|
|
231
|
+
* Download DTD zip file from NIH FTP server
|
|
232
|
+
*/
|
|
107
233
|
function dtdDownload(session, opts) {
|
|
108
234
|
return __awaiter(this, void 0, void 0, function* () {
|
|
109
235
|
if (!fs_1.default.existsSync(opts.directory)) {
|
|
@@ -115,6 +241,9 @@ function dtdDownload(session, opts) {
|
|
|
115
241
|
(0, myst_cli_utils_1.writeFileToFolder)(localDtdZipFile(opts), yield resp.buffer());
|
|
116
242
|
});
|
|
117
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Download DTD zip file from NIH FTP server if it does not yet exist
|
|
246
|
+
*/
|
|
118
247
|
function ensureDtdZipExists(session, opts) {
|
|
119
248
|
return __awaiter(this, void 0, void 0, function* () {
|
|
120
249
|
if (!fs_1.default.existsSync(path_1.default.join(opts.directory, dtdZipFile(opts)))) {
|
|
@@ -122,32 +251,48 @@ function ensureDtdZipExists(session, opts) {
|
|
|
122
251
|
}
|
|
123
252
|
});
|
|
124
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Download and extract DTD file if it does not yet exist
|
|
256
|
+
*/
|
|
125
257
|
function ensureDtdExists(session, opts) {
|
|
126
258
|
return __awaiter(this, void 0, void 0, function* () {
|
|
127
259
|
if (!fs_1.default.existsSync(localDtdFile(opts))) {
|
|
128
260
|
yield ensureDtdZipExists(session, opts);
|
|
129
261
|
const zipFile = localDtdZipFile(opts);
|
|
130
|
-
session.log.info(`🤐 Unzipping template
|
|
262
|
+
session.log.info(`🤐 Unzipping template: ${zipFile}`);
|
|
131
263
|
yield (0, fs_1.createReadStream)(zipFile)
|
|
132
264
|
.pipe(unzipper_1.default.Extract({ path: opts.directory }))
|
|
133
265
|
.promise();
|
|
134
266
|
}
|
|
135
|
-
session.log.debug(`Validating against ${localDtdFile(opts)}`);
|
|
136
267
|
});
|
|
137
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Test if xmllint is available as a cli command
|
|
271
|
+
*/
|
|
138
272
|
function isXmllintAvailable() {
|
|
139
273
|
return (0, which_1.sync)('xmllint', { nothrow: true });
|
|
140
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Check if JATS file is valid based on JATS version/library/etc.
|
|
277
|
+
*
|
|
278
|
+
* Returns true if valid and false if invalid.
|
|
279
|
+
*/
|
|
141
280
|
function validateJatsAgainstDtd(session, file, opts) {
|
|
142
281
|
return __awaiter(this, void 0, void 0, function* () {
|
|
143
282
|
if (!isXmllintAvailable()) {
|
|
144
283
|
session.log.error(`JATS validation against DTD requires xmllint\n\n${chalk_1.default.dim('To install:\n mac: brew install xmlstarlet\n debian: apt install libxml2-utils')}`);
|
|
145
284
|
return;
|
|
146
285
|
}
|
|
147
|
-
const
|
|
286
|
+
const inferredOpts = inferOptions(file);
|
|
287
|
+
const validatedOpts = validateOptions(session, opts !== null && opts !== void 0 ? opts : {}, inferredOpts);
|
|
148
288
|
yield ensureDtdExists(session, validatedOpts);
|
|
289
|
+
session.log.debug(`Validating against: ${localDtdFile(validatedOpts)}`);
|
|
290
|
+
session.log.info(`🧐 Validating against: ${dtdFolder(validatedOpts)}`);
|
|
149
291
|
try {
|
|
150
|
-
|
|
292
|
+
// First drop DOCTYPE with DTD in it - we have already fetched the DTD
|
|
293
|
+
const dropDtdCommand = `xmllint --dropdtd`;
|
|
294
|
+
const validateCommand = `xmllint --noout --dtdvalid ${localDtdFile(validatedOpts)}`;
|
|
295
|
+
yield (0, myst_cli_utils_1.makeExecutable)(`${dropDtdCommand} ${file} | ${validateCommand} -`, session.log)();
|
|
151
296
|
}
|
|
152
297
|
catch (_a) {
|
|
153
298
|
return false;
|
|
@@ -156,6 +301,11 @@ function validateJatsAgainstDtd(session, file, opts) {
|
|
|
156
301
|
});
|
|
157
302
|
}
|
|
158
303
|
exports.validateJatsAgainstDtd = validateJatsAgainstDtd;
|
|
304
|
+
/**
|
|
305
|
+
* Check if JATS file is valid based on JATS version/library/etc.
|
|
306
|
+
*
|
|
307
|
+
* Logs confirmation message if valid and throws an error if invalid.
|
|
308
|
+
*/
|
|
159
309
|
function validateJatsAgainstDtdWrapper(session, file, opts) {
|
|
160
310
|
return __awaiter(this, void 0, void 0, function* () {
|
|
161
311
|
const success = yield validateJatsAgainstDtd(session, file, opts);
|
package/dist/cjs/version.js
CHANGED
package/dist/esm/cli/index.js
CHANGED
|
@@ -3,9 +3,11 @@ import commander from 'commander';
|
|
|
3
3
|
import version from '../version';
|
|
4
4
|
import { addDownloadCLI } from './parse';
|
|
5
5
|
import { addValidateCLI } from './validate';
|
|
6
|
+
import { addTestCLI } from './jats-test';
|
|
6
7
|
const program = new commander.Command();
|
|
7
8
|
addDownloadCLI(program);
|
|
8
9
|
addValidateCLI(program);
|
|
10
|
+
addTestCLI(program);
|
|
9
11
|
program.version(`v${version}`, '-v, --version', 'Print the current version of jats-xml');
|
|
10
12
|
program.option('-d, --debug', 'Log out any errors to the console.');
|
|
11
13
|
program.parse(process.argv);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { Command, Option } from 'commander';
|
|
11
|
+
import { clirun, tic } from 'myst-cli-utils';
|
|
12
|
+
import { getSession } from '../session';
|
|
13
|
+
import yaml from 'js-yaml';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import { parseJats } from './parse';
|
|
16
|
+
import { select, selectAll } from 'unist-util-select';
|
|
17
|
+
import { is } from 'unist-util-is';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
const INDENT = ' ';
|
|
20
|
+
function printNodes(expected, received) {
|
|
21
|
+
return chalk.reset(`\n${INDENT}${chalk.greenBright('Expected node containing')}:\n${INDENT} ${yaml
|
|
22
|
+
.dump(expected)
|
|
23
|
+
.replace(/\n/g, `\n${INDENT} `)}\n${INDENT}${chalk.redBright('Received node')}:\n${INDENT} ${yaml.dump(received).replace(/\n/g, `\n${INDENT} `)}`);
|
|
24
|
+
}
|
|
25
|
+
export function testJatsFile(session, file, opts) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
const toc = tic();
|
|
28
|
+
const jats = yield parseJats(session, file);
|
|
29
|
+
const tests = yaml.load(fs.readFileSync(opts.cases).toString());
|
|
30
|
+
const results = tests.cases.map((testCase, index) => {
|
|
31
|
+
if (!testCase.title) {
|
|
32
|
+
return [`Test Case ${index}`, null, 'Test must include a title'];
|
|
33
|
+
}
|
|
34
|
+
if (testCase.equals === undefined) {
|
|
35
|
+
return [testCase.title, null, 'Test must have an equals statement'];
|
|
36
|
+
}
|
|
37
|
+
if (testCase.select) {
|
|
38
|
+
const node = select(testCase.select, jats.tree);
|
|
39
|
+
const pass = is(node, testCase.equals);
|
|
40
|
+
if (testCase.equals == null && node) {
|
|
41
|
+
return [testCase.title, false, 'Expected no node to be present'];
|
|
42
|
+
}
|
|
43
|
+
if (!node && testCase.equals == null)
|
|
44
|
+
return [testCase.title, true];
|
|
45
|
+
if (!node)
|
|
46
|
+
return [testCase.title, false];
|
|
47
|
+
let failed = false;
|
|
48
|
+
const messages = [];
|
|
49
|
+
if (!pass) {
|
|
50
|
+
failed = failed || true;
|
|
51
|
+
messages.push(`Failed to validate node\n${printNodes(testCase.equals, node)}`);
|
|
52
|
+
}
|
|
53
|
+
return [testCase.title, !failed, messages.join('\n')];
|
|
54
|
+
}
|
|
55
|
+
else if (testCase.selectAll) {
|
|
56
|
+
const testNodes = selectAll(testCase.selectAll, jats.tree);
|
|
57
|
+
if (!testNodes && testCase.equals == null)
|
|
58
|
+
return [testCase.title, true];
|
|
59
|
+
if (!testNodes)
|
|
60
|
+
return [testCase.title, false, 'Node not found'];
|
|
61
|
+
let equals = testCase.equals;
|
|
62
|
+
if (!Array.isArray(testCase.equals)) {
|
|
63
|
+
equals = Array(testNodes.length).fill(testCase.equals);
|
|
64
|
+
}
|
|
65
|
+
let failed = false;
|
|
66
|
+
const messages = [];
|
|
67
|
+
if (equals.length !== testNodes.length) {
|
|
68
|
+
failed = failed || true;
|
|
69
|
+
messages.push(`Expected ${equals.length} nodes, got ${testNodes.length}\n${printNodes(equals, testNodes)}`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
equals.forEach((node, ii) => {
|
|
73
|
+
const pass = is(testNodes[ii], node);
|
|
74
|
+
if (!pass) {
|
|
75
|
+
failed = failed || true;
|
|
76
|
+
messages.push(`Failed to validate node ${ii}\n${printNodes(node, testNodes[ii])}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return [testCase.title, !failed, messages.join('\n')];
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
return [testCase.title, false, 'Test must have either `select` or `selectAll`'];
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
results.forEach((result) => {
|
|
87
|
+
const [title, pass, message] = result;
|
|
88
|
+
if (pass === null)
|
|
89
|
+
session.log.info(`${chalk.redBright.bold(`ERROR`)} - ${title}\n ${chalk.blueBright(message)}`);
|
|
90
|
+
else if (pass)
|
|
91
|
+
session.log.info(`${chalk.green(`PASS`)} - ${title}`);
|
|
92
|
+
else
|
|
93
|
+
session.log.info(`${chalk.red(`FAIL`)} - ${title}\n\n${INDENT}${chalk.blueBright(message)}\n`);
|
|
94
|
+
}, true);
|
|
95
|
+
const passed = results.reduce((num, [, pass]) => num + (pass ? 1 : 0), 0);
|
|
96
|
+
const failed = results.length - passed;
|
|
97
|
+
if (failed > 0 && passed === 0) {
|
|
98
|
+
throw new Error(toc(`${chalk.red(`Failed ${failed} tests in %s`)} 👎`));
|
|
99
|
+
}
|
|
100
|
+
if (failed > 0) {
|
|
101
|
+
throw new Error(toc(`${chalk.green(`Passed ${passed}/${results.length} tests in %s`)}\n${chalk.red(`Failed ${failed} tests`)} 👎`));
|
|
102
|
+
}
|
|
103
|
+
session.log.info(chalk.green(toc(`Passed ${passed} tests in %s 🚀`)));
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function makeTestCLI(program) {
|
|
108
|
+
const command = new Command('test')
|
|
109
|
+
.description('Test JATS file against a list of cases')
|
|
110
|
+
.argument('<file>', 'JATS file to test')
|
|
111
|
+
.addOption(new Option('--cases <value>', 'The YAML file of unit tests to test against'))
|
|
112
|
+
.action(clirun(testJatsFile, { program, getSession }));
|
|
113
|
+
return command;
|
|
114
|
+
}
|
|
115
|
+
export function addTestCLI(program) {
|
|
116
|
+
program.addCommand(makeTestCLI(program));
|
|
117
|
+
}
|
package/dist/esm/cli/parse.js
CHANGED
|
@@ -53,7 +53,7 @@ function logAboutJatsFailing(session, jatsUrls) {
|
|
|
53
53
|
session.log.debug(formatPrinciples('A*', { chalk }));
|
|
54
54
|
session.log.info(`\n${chalk.blue('The link may work in a browser.')}\n`);
|
|
55
55
|
}
|
|
56
|
-
function parseJats(session, file, opts = { resolvers: DEFAULT_RESOLVERS }) {
|
|
56
|
+
export function parseJats(session, file, opts = { resolvers: DEFAULT_RESOLVERS }) {
|
|
57
57
|
return __awaiter(this, void 0, void 0, function* () {
|
|
58
58
|
const toc = tic();
|
|
59
59
|
if (fs.existsSync(file)) {
|