1 | ;
|
2 |
|
3 | var ono = require('ono'),
|
4 | url = require('./util/url'),
|
5 | plugins = require('./util/plugins');
|
6 |
|
7 | module.exports = parse;
|
8 |
|
9 | /**
|
10 | * Reads and parses the specified file path or URL.
|
11 | *
|
12 | * @param {string} path - This path MUST already be resolved, since `read` doesn't know the resolution context
|
13 | * @param {$Refs} $refs
|
14 | * @param {$RefParserOptions} options
|
15 | *
|
16 | * @returns {Promise}
|
17 | * The promise resolves with the parsed file contents, NOT the raw (Buffer) contents.
|
18 | */
|
19 | function parse (path, $refs, options) {
|
20 | try {
|
21 | // Remove the URL fragment, if any
|
22 | path = url.stripHash(path);
|
23 |
|
24 | // Add a new $Ref for this file, even though we don't have the value yet.
|
25 | // This ensures that we don't simultaneously read & parse the same file multiple times
|
26 | var $ref = $refs._add(path);
|
27 |
|
28 | // This "file object" will be passed to all resolvers and parsers.
|
29 | var file = {
|
30 | url: path,
|
31 | extension: url.getExtension(path),
|
32 | };
|
33 |
|
34 | // Read the file and then parse the data
|
35 | return readFile(file, options)
|
36 | .then(function (resolver) {
|
37 | $ref.pathType = resolver.plugin.name;
|
38 | file.data = resolver.result;
|
39 | return parseFile(file, options);
|
40 | })
|
41 | .then(function (parser) {
|
42 | $ref.value = parser.result;
|
43 | return parser.result;
|
44 | });
|
45 | }
|
46 | catch (e) {
|
47 | return Promise.reject(e);
|
48 | }
|
49 | }
|
50 |
|
51 | /**
|
52 | * Reads the given file, using the configured resolver plugins
|
53 | *
|
54 | * @param {object} file - An object containing information about the referenced file
|
55 | * @param {string} file.url - The full URL of the referenced file
|
56 | * @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
|
57 | * @param {$RefParserOptions} options
|
58 | *
|
59 | * @returns {Promise}
|
60 | * The promise resolves with the raw file contents and the resolver that was used.
|
61 | */
|
62 | function readFile (file, options) {
|
63 | return new Promise(function (resolve, reject) {
|
64 | // console.log('Reading %s', file.url);
|
65 |
|
66 | // Find the resolvers that can read this file
|
67 | var resolvers = plugins.all(options.resolve);
|
68 | resolvers = plugins.filter(resolvers, 'canRead', file);
|
69 |
|
70 | // Run the resolvers, in order, until one of them succeeds
|
71 | plugins.sort(resolvers);
|
72 | plugins.run(resolvers, 'read', file)
|
73 | .then(resolve, onError);
|
74 |
|
75 | function onError (err) {
|
76 | // Throw the original error, if it's one of our own (user-friendly) errors.
|
77 | // Otherwise, throw a generic, friendly error.
|
78 | if (err && !(err instanceof SyntaxError)) {
|
79 | reject(err);
|
80 | }
|
81 | else {
|
82 | reject(ono.syntax('Unable to resolve $ref pointer "%s"', file.url));
|
83 | }
|
84 | }
|
85 | });
|
86 | }
|
87 |
|
88 | /**
|
89 | * Parses the given file's contents, using the configured parser plugins.
|
90 | *
|
91 | * @param {object} file - An object containing information about the referenced file
|
92 | * @param {string} file.url - The full URL of the referenced file
|
93 | * @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
|
94 | * @param {*} file.data - The file contents. This will be whatever data type was returned by the resolver
|
95 | * @param {$RefParserOptions} options
|
96 | *
|
97 | * @returns {Promise}
|
98 | * The promise resolves with the parsed file contents and the parser that was used.
|
99 | */
|
100 | function parseFile (file, options) {
|
101 | return new Promise(function (resolve, reject) {
|
102 | // console.log('Parsing %s', file.url);
|
103 |
|
104 | // Find the parsers that can read this file type.
|
105 | // If none of the parsers are an exact match for this file, then we'll try ALL of them.
|
106 | // This handles situations where the file IS a supported type, just with an unknown extension.
|
107 | var allParsers = plugins.all(options.parse);
|
108 | var filteredParsers = plugins.filter(allParsers, 'canParse', file);
|
109 | var parsers = filteredParsers.length > 0 ? filteredParsers : allParsers;
|
110 |
|
111 | // Run the parsers, in order, until one of them succeeds
|
112 | plugins.sort(parsers);
|
113 | plugins.run(parsers, 'parse', file)
|
114 | .then(onParsed, onError);
|
115 |
|
116 | function onParsed (parser) {
|
117 | if (!parser.plugin.allowEmpty && isEmpty(parser.result)) {
|
118 | reject(ono.syntax('Error parsing "%s" as %s. \nParsed value is empty', file.url, parser.plugin.name));
|
119 | }
|
120 | else {
|
121 | resolve(parser);
|
122 | }
|
123 | }
|
124 |
|
125 | function onError (err) {
|
126 | if (err) {
|
127 | err = err instanceof Error ? err : new Error(err);
|
128 | reject(ono.syntax(err, 'Error parsing %s', file.url));
|
129 | }
|
130 | else {
|
131 | reject(ono.syntax('Unable to parse %s', file.url));
|
132 | }
|
133 | }
|
134 | });
|
135 | }
|
136 |
|
137 | /**
|
138 | * Determines whether the parsed value is "empty".
|
139 | *
|
140 | * @param {*} value
|
141 | * @returns {boolean}
|
142 | */
|
143 | function isEmpty (value) {
|
144 | return value === undefined ||
|
145 | (typeof value === 'object' && Object.keys(value).length === 0) ||
|
146 | (typeof value === 'string' && value.trim().length === 0) ||
|
147 | (Buffer.isBuffer(value) && value.length === 0);
|
148 | }
|