└── scripts └── md2html └── md2html.js /scripts/md2html/md2html.js: -------------------------------------------------------------------------------- 1 | /* ReSpec supports markdown formatting, but this shows up on the page before being rendered 2 | Hence we render the markdown to HTML ourselves, this gives us 3 | complete control over formatting and syntax highlighting */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * @author Mike Ralphson 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 += ``; 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 = '
'; 457 | // adjust the heading to be at level 2 because respec insists on h2 here 458 | // Note: older specs had this at h4, newer specs at h2, and all heading levels have been reduced by 1 in the preceding block 459 | line = '#' + m[0]; 460 | delta = 1; 461 | heading = 2; 462 | } 463 | if (line.includes('Appendix')) { 464 | newSection = '
'; 465 | } 466 | 467 | // heading level delta is either 0 or is +1/-1, or we're in respec mode 468 | // respec insists on
...
breaks around headings 469 | 470 | if (delta === 0) { 471 | prefix = '
'+newSection; 472 | } 473 | else if (delta > 0) { 474 | prefix = newSection.repeat(delta); 475 | } 476 | else { 477 | prefix = '
'+('
').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 | --------------------------------------------------------------------------------