9 | **/
10 |
11 | const fs = require('fs');
12 | const path = require('path');
13 | const url = require('url');
14 |
15 | const hljs = require('highlight.js');
16 | hljs.registerLanguage('uritemplate', function() {
17 | return {
18 | case_insensitive: true,
19 | contains: [
20 | {
21 | scope: "attr",
22 | match: /(?<=[{,])[^,}\n\r]+/,
23 | }
24 | ],
25 | }
26 | });
27 | hljs.registerLanguage('uri', function() {
28 | return {
29 | case_insensitive: true,
30 | classNameAliases: {
31 | pathsegment: "attr",
32 | option: "attr",
33 | value: "literal"
34 | },
35 | contains: [
36 | {
37 | scope: "pathsegment",
38 | match: /(?<=[/])[^/?#\n\r]+/,
39 | },
40 | {
41 | scope: "option",
42 | match: /(?<=[?])[^=?\n\r]+/,
43 | },
44 | {
45 | scope: "value",
46 | match: /(?<=\=)[^?\n\r]+/,
47 | }
48 | ],
49 | }
50 | });
51 | hljs.registerLanguage('multipart', function() {
52 | return {
53 | // This is a very limited approach that only
54 | // detects boundaries and headers that start
55 | // with "Content-"
56 | contains: [
57 | {
58 | scope: "meta",
59 | match: /^--.*$/,
60 | },
61 | {
62 | scope: "literal",
63 | begin: /^Content-/,
64 | end: /$/,
65 | contains: [
66 | {
67 | scope: "attr",
68 | begin: " ",
69 | end: /$/,
70 | },
71 | ]
72 | },
73 | ],
74 | }
75 | });
76 | hljs.registerLanguage('eventstream', function() {
77 | return {
78 | contains: [
79 | {
80 | scope: "comment",
81 | begin: /^:/,
82 | end: /$/,
83 | },
84 | {
85 | scope: "attr",
86 | match: /^[^:]+/
87 | },
88 | ],
89 | }
90 | });
91 | hljs.registerLanguage('jsonseq', function() {
92 | return {
93 | keywords: ["true", "false", "null"],
94 | contains: [
95 | {
96 | scope: "meta",
97 | match: /0[xX]1[eE]/,
98 | },
99 | {
100 | scope: "attr",
101 | begin: /"(\\.|[^\\"\r\n])*"(?=\s*:)/,
102 | relevance: 1.01
103 | },
104 | {
105 | scope: "punctuation",
106 | match: /[{}[\],:]/,
107 | relevance: 0
108 | },
109 | {
110 | scope: "literals",
111 | beginKeywords: ["true", "false" , "null"].join(" "),
112 | },
113 | hljs.QUOTE_STRING_MODE,
114 | hljs.C_NUMBER_MODE
115 | ]
116 | }
117 | });
118 | hljs.registerLanguage('jsonl', function() {
119 | return {
120 | aliases: ["ndjson"],
121 | keywords: ["true", "false", "null"],
122 | contains: [
123 | {
124 | scope: 'attr',
125 | begin: /"(\\.|[^\\"\r\n])*"(?=\s*:)/,
126 | relevance: 1.01
127 | },
128 | {
129 | scope: "punctuation",
130 | match: /[{}[\],:]/,
131 | relevance: 0
132 | },
133 | {
134 | scope: "literals",
135 | beginKeywords: ["true", "false" , "null"].join(" "),
136 | },
137 | hljs.QUOTE_STRING_MODE,
138 | hljs.C_NUMBER_MODE
139 | ]
140 | }
141 | });
142 |
143 |
144 | const cheerio = require('cheerio');
145 |
146 | let argv = require('yargs')(process.argv.slice(2))
147 | .string('maintainers')
148 | .alias('m','maintainers')
149 | .describe('maintainers','path to MAINTAINERS.md')
150 | .demandCommand(1)
151 | .parse();
152 | const abstract = 'What is the OpenAPI Specification?';
153 | let maintainers = [];
154 | let emeritus = [];
155 |
156 | const md = require('markdown-it')({
157 | html: true,
158 | linkify: true,
159 | typographer: true,
160 | highlight: function (str, lang) {
161 | if (lang && hljs.getLanguage(lang)) {
162 | return '' +
163 | hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
164 | '
';
165 | }
166 |
167 | if (lang) console.warn('highlight.js does not support language',lang);
168 | return '' + md.utils.escapeHtml(str) + '
';
169 | }
170 | });
171 |
172 | function preface(title,options) {
173 | const otherVersions = options._[1].split("\n").map(v => path.basename(v,'.md')).filter(v => v !== options.subtitle);
174 | const respec = {
175 | specStatus: "base",
176 | latestVersion: "https://spec.openapis.org/oas/latest.html",
177 | thisVersion: `https://spec.openapis.org/oas/v${options.subtitle}.html`,
178 | canonicalURI: `https://spec.openapis.org/oas/v${options.subtitle}.html`,
179 | editors: maintainers,
180 | formerEditors: emeritus,
181 | publishDate: options.publishDate,
182 | subtitle: 'Version '+options.subtitle,
183 | edDraftURI: "https://github.com/OAI/OpenAPI-Specification/",
184 | shortName: "OAS",
185 | historyURI: null, // prevent ReSpec from fetching a W3C history based on the shortName
186 | lint: false,
187 | logos:[{
188 | src: "https://raw.githubusercontent.com/OAI/OpenAPI-Style-Guide/master/graphics/bitmap/OpenAPI_Logo_Pantone.png",
189 | alt: "OpenAPI Initiative",
190 | height: 48,
191 | url: "https://openapis.org/"}],
192 | otherLinks: [
193 | {
194 | key: "Other versions:",
195 | data: otherVersions.map(v => {
196 | return {
197 | href: `https://spec.openapis.org/oas/v${v}.html`
198 | }
199 | })
200 | },
201 | {
202 | key: "Participate",
203 | data: [
204 | {
205 | value: "GitHub OAI/OpenAPI-Specification",
206 | href: "https://github.com/OAI/OpenAPI-Specification/",
207 | },
208 | {
209 | value: "File a bug",
210 | href: "https://github.com/OAI/OpenAPI-Specification/issues",
211 | },
212 | {
213 | value: "Commit history",
214 | href: `https://github.com/OAI/OpenAPI-Specification/commits/main/versions/${options.subtitle}.md`,
215 | },
216 | {
217 | value: "Pull requests",
218 | href: "https://github.com/OAI/OpenAPI-Specification/pulls",
219 | },
220 | ],
221 | },
222 | ],
223 | // localBiblio: {
224 | // // add local bibliography entries here, add them to https://www.specref.org/, and remove them here once published
225 | // }
226 | };
227 |
228 | let preface = '\n'
229 | preface += fs.readFileSync(path.resolve(__dirname,'./analytics/google.html'),'utf8');
230 |
231 | // SEO
232 | preface += `\n${md.utils.escapeHtml(title)}`;
233 | preface += '\n';
234 |
235 | // ReSpec
236 | preface += '';
237 | preface += '';
238 | preface += `\n`;
239 | preface += '\n';
240 | preface += '';
244 | preface += `${title.split('|')[0]}
`;
245 | preface += `Copyright © ${options.publishDate.getFullYear()} the Linux Foundation
`;
246 | preface += `${abstract}
`;
247 | preface += 'The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic. When properly defined via OpenAPI, a consumer can understand and interact with the remote service with a minimal amount of implementation logic. Similar to what interface descriptions have done for lower-level programming, the OpenAPI Specification removes guesswork in calling a service.';
248 | preface += '';
249 | preface += '';
250 | preface += 'Status of This Document
';
251 | preface += 'The source-of-truth for this specification is the HTML file referenced above as This version.';
252 | preface += '';
253 |
254 | return preface;
255 | }
256 |
257 | function doMaintainers() {
258 | let m = fs.readFileSync(argv.maintainers,'utf8');
259 | let h = md.render(m);
260 | let $ = cheerio.load(h);
261 | let u = $('ul').first();
262 | $(u).children('li').each(function(e){
263 | let t = $(this).text().split('@')[0];
264 | maintainers.push({name:t});
265 | });
266 | if ($("ul").length < 2) return;
267 | u = $("ul").last();
268 | $(u).children('li').each(function(e){
269 | let t = $(this).text().split('@')[0];
270 | emeritus.push({name:t});
271 | });
272 | }
273 |
274 | function getPublishDate(m) {
275 | let result = new Date();
276 | let h = md.render(m);
277 | let $ = cheerio.load(h);
278 | $('table').each(function(i,table){
279 | const h = $(table).find('th');
280 | const headers = [];
281 | $(h).each(function(i,header){
282 | headers.push($(header).text());
283 | });
284 | if (headers.length >= 2 && headers[0] === 'Version' && headers[1] === 'Date') {
285 | let c = $(table).find('tr').find('td');
286 | let v = $(c[0]).text();
287 | let d = $(c[1]).text();
288 | argv.subtitle = v;
289 | if (d !== 'TBA') result = new Date(d);
290 | }
291 | });
292 | return result;
293 | }
294 |
295 | if (argv.maintainers) {
296 | doMaintainers();
297 | }
298 |
299 | let s = fs.readFileSync(argv._[0],'utf8');
300 |
301 | argv.publishDate = getPublishDate(s);
302 |
303 | let lines = s.split(/\r?\n/);
304 |
305 | let prevHeading = 0;
306 | let inTOC = false;
307 | let inDefs = false;
308 | let inCodeBlock = false;
309 | let indents = [0];
310 |
311 | // process the markdown
312 | for (let l in lines) {
313 | let line = lines[l];
314 |
315 | // remove TOC from older spec versions, respec will generate a new one
316 | if (line.startsWith('## Table of Contents')) inTOC = true;
317 | else if (line.startsWith('#')) inTOC = false;
318 | if (inTOC) line = '';
319 |
320 | // special formatting for Definitions section
321 | if (line.startsWith('## Definitions')) {
322 | inDefs = true;
323 | }
324 | else if (line.startsWith('## ')) inDefs = false;
325 |
326 | // recognize code blocks
327 | if (line.startsWith('```')) {
328 | inCodeBlock = !inCodeBlock;
329 | }
330 |
331 | if (line.indexOf('')>=0) {
332 | // fix syntax error in 2.0.md
333 | line = line.replace('','');
334 | }
335 |
336 | // replace deprecated with - needed for older specs
337 | line = line.replace(/<\/a>/g,'');
338 |
339 | line = line.split('\\|').join('|'); // was ¦
340 |
341 | if (!inCodeBlock) {
342 |
343 | // minor fixups to get RFC links to work properly
344 | line = line.replace('RFC [','[RFC');
345 | line = line.replace('[Authorization header as defined in ','Authorization header as defined in [');
346 | line = line.replace('[JSON Pointer]','JSON Pointer [RFC6901]'); // only in 2.0.md
347 | line = line.replace('[media type range](https://tools.ietf.org/html/rfc7231#appendix-D) ','media type range, see [RFC7231](https://tools.ietf.org/html/rfc7231#appendix-D), ');
348 |
349 | line = line.replace(/\[RFC ?([0-9]{1,5})\]\(/g,'[[RFC$1]](');
350 |
351 | // harmonize RFC URLs
352 | //TODO: harmonize to https://www.rfc-editor.org/rfc/rfc*
353 | line = line.replaceAll('](http://','](https://');
354 | line = line.replace('https://www.ietf.org/rfc/rfc2119.txt','https://tools.ietf.org/html/rfc2119'); // only in 2.0.md
355 | line = line.replace(/https:\/\/www.rfc-editor.org\/rfc\/rfc([0-9]{1,5})(\.html)?/g,'https://tools.ietf.org/html/rfc$1');
356 | line = line.replaceAll('https://datatracker.ietf.org/doc/html/','https://tools.ietf.org/html/');
357 |
358 | // handle url fragments in RFC links and construct section links as well as RFC links
359 | line = line.replace(/\]\]\(https:\/\/tools.ietf.org\/html\/rfc([0-9]{1,5})\/?(\#[^)]*)?\)/g, function(match, rfcNumber, fragment) {
360 | if (fragment) {
361 | // Extract section title from the fragment
362 | let sectionTitle = fragment.replace('#', '').replace(/-/g, ' ');
363 | sectionTitle = sectionTitle.charAt(0).toUpperCase() + sectionTitle.slice(1); // Capitalize the first letter
364 | //TODO: section links to https://www.rfc-editor.org/rfc/rfc* for newer RFCs (>= 8700)
365 | return `]] [${sectionTitle}](https://datatracker.ietf.org/doc/html/rfc${rfcNumber}${fragment})`;
366 | } else {
367 | return ']]';
368 | }
369 | });
370 |
371 | // non-RFC references
372 | line = line.replace('[ABNF](https://tools.ietf.org/html/rfc5234)','[[ABNF]]');
373 | line = line.replace('[CommonMark 0.27](https://spec.commonmark.org/0.27/)','[[CommonMark-0.27]]');
374 | line = line.replace('[CommonMark syntax](https://spec.commonmark.org/)','[[CommonMark]] syntax');
375 | line = line.replace('CommonMark markdown formatting','[[CommonMark]] markdown formatting');
376 | line = line.replace('consult http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4)','consult [[HTML401]] [Section 17.13.4](http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4)');
377 | line = line.replace('[IANA Status Code Registry](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml)','[[IANA-HTTP-STATUS-CODES|IANA Status Code Registry]]');
378 | line = line.replace('[IANA Authentication Scheme registry](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml)','[[IANA-HTTP-AUTHSCHEMES]]');
379 | line = line.replace('[JSON Reference](https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03)','[[JSON-Reference|JSON Reference]]');
380 | line = line.replace('[JSON Schema Specification Draft 4](https://json-schema.org/)','[[JSON-Schema-04|JSON Schema Specification Draft 4]]');
381 | line = line.replace('[JSON Schema Core](https://tools.ietf.org/html/draft-zyp-json-schema-04)','[[JSON-Schema-04|JSON Schema Core]]');
382 | line = line.replace('[JSON Schema Validation](https://tools.ietf.org/html/draft-fge-json-schema-validation-00)','[[JSON-Schema-Validation-04|JSON Schema Validation]]');
383 | line = line.replace('[JSON Schema Specification Wright Draft 00](https://json-schema.org/)','[[JSON-Schema-05|JSON Schema Specification Wright Draft 00]]');
384 | line = line.replace('[JSON Schema Core](https://tools.ietf.org/html/draft-wright-json-schema-00)','[[JSON-Schema-05|JSON Schema Core]]');
385 | line = line.replace('[JSON Schema Validation](https://tools.ietf.org/html/draft-wright-json-schema-validation-00)','[[JSON-Schema-Validation-05|JSON Schema Validation]]');
386 | line = line.replace('[JSON Schema Specification Draft 2020-12](https://tools.ietf.org/html/draft-bhutton-json-schema-00)','[[JSON-Schema-2020-12|JSON Schema Specification Draft 2020-12]]');
387 | line = line.replace('[JSON Schema Core](https://tools.ietf.org/html/draft-bhutton-json-schema-00)','[[JSON-Schema-2020-12|JSON Schema Core]]');
388 | line = line.replace('[JSON Schema Validation](https://tools.ietf.org/html/draft-bhutton-json-schema-validation-00)','[[JSON-Schema-Validation-2020-12|JSON Schema Validation]]');
389 | line = line.replace('[SPDX](https://spdx.org/licenses/) license','[[SPDX-Licenses]]');
390 | line = line.replace('[XML namespaces](https://www.w3.org/TR/xml-names11/)','[[xml-names11|XML namespaces]]');
391 | line = line.replace('JSON standards. YAML,','[[RFC7159|JSON]] standards. [[YAML|YAML]],'); // 2.0.md only
392 | line = line.replace('JSON or YAML format.','[[RFC7159|JSON]] or [[YAML|YAML]] format.');
393 | line = line.replace(/YAML version \[1\.2\]\(https:\/\/(www\.)?yaml\.org\/spec\/1\.2\/spec\.html\)/,'[[YAML|YAML version 1.2]]');
394 | }
395 |
396 | // fix relative links (to examples)
397 | if (!inCodeBlock && line.indexOf('](../examples/') >= 0) {
398 | // links to examples go to learn site, links to yaml files go to wrapper html
399 | line = line.replace(/\(\.\.\/examples\/([^)]+)\)/g,function(match,group1){
400 | console.warn("example link",group1);
401 | group1 = group1.replace('.yaml','.html');
402 | return `(https://learn.openapis.org/examples/${group1})`;
403 | })
404 | } else if (!inCodeBlock && line.indexOf('](../') >= 0) {
405 | // links to other sibling files go to github
406 | const regExp = /\((\.\.[^)]+)\)/g;
407 | line = line.replace(regExp,function(match,group1){
408 | console.warn('relative link',group1);
409 | return '('+url.resolve('https://github.com/OAI/OpenAPI-Specification/tree/main/versions/foo',group1)+')';
410 | });
411 | }
412 |
413 | // fix indentation of headings
414 | // - make sure that each heading is at most one level deeper than the previous heading
415 | // - reduce heading level by one if we're in respec mode except for h1
416 | if (!inCodeBlock && line.startsWith('#')) {
417 | let indent = 0;
418 | while (line[indent] === '#') indent++;
419 | let originalIndent = indent;
420 |
421 | let prevIndent = indents[indents.length-1]; // peek
422 | let delta = indent-prevIndent;
423 |
424 | if (indent > 1) {
425 | indent--;
426 | }
427 | let newIndent = indent;
428 |
429 | let title = line.split('# ')[1];
430 | if (inDefs) title = ''+title+'';
431 | line = ('#'.repeat(newIndent)+' '+title);
432 |
433 | if (delta>0) indents.push(originalIndent);
434 | if (delta<0) {
435 | let d = Math.abs(delta);
436 | while (d>0) {
437 | indents.pop();
438 | d--;
439 | }
440 | }
441 | }
442 |
443 | // wrap section text in tags for respec
444 | if (!inCodeBlock && line.startsWith('#')) {
445 | let heading = 0;
446 | while (line[heading] === '#') heading++;
447 | let delta = heading-prevHeading;
448 | if (delta>1) console.warn(delta,line);
449 | if (delta>0) delta = 1;
450 | let prefix = '';
451 | let newSection = '';
452 | const m = line.match(/# Version ([0-9.]+)$/);
453 | if (m) {
454 | // our conformance section is headlined with 'Version x.y.z'
455 | // and respec needs a conformance section in a "formal" specification
456 | newSection = ''+('').repeat(Math.abs(delta))+newSection;
478 | }
479 | prevHeading = heading;
480 | line = prefix+md.render(line);
481 | }
482 |
483 | lines[l] = line;
484 | }
485 |
486 | s = preface(`OpenAPI Specification v${argv.subtitle} | Introduction, Definitions, & More`,argv)+'\n\n'+lines.join('\n');
487 | let out = md.render(s);
488 | out = out.replace(/\[([RGB])\]/g,'[$1]');
489 | out = out.replace('[[IANA-HTTP-AUTHSCHEMES]]','[[IANA-HTTP-AUTHSCHEMES|IANA Authentication Scheme registry]]');
490 | console.log(out);
491 |
--------------------------------------------------------------------------------