UNPKG

37.1 kBJavaScriptView Raw
1/*
2 * Copyright 2019 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12const {
13 map, list: flist, flat, filter, size, foldl,
14} = require('ferrum');
15const {
16 root, paragraph, text, heading, code, table,
17 tableRow, tableCell, link, inlineCode, list, listItem, strong,
18 blockquote,
19} = require('mdast-builder');
20const i18n = require('es2015-i18n-tag').default;
21const ghslugger = require('github-slugger');
22const yaml = require('js-yaml');
23const s = require('./symbols');
24const { gentitle } = require('./formattingTools');
25const { keyword } = require('./keywords');
26
27function build({
28 header,
29 links = {},
30 includeproperties = [],
31 rewritelinks = (x) => x,
32 exampleformat = 'json',
33 skipproperties = [],
34} = {}) {
35 const formats = {
36 'date-time': {
37 label: i18n`date time`,
38 text: i18n`the string must be a date time string, according to `,
39 specname: 'RFC 3339, section 5.6',
40 speclink: 'https://tools.ietf.org/html/rfc3339',
41 },
42 date: {
43 label: i18n`date`,
44 text: i18n`the string must be a date string, according to `,
45 specname: 'RFC 3339, section 5.6',
46 speclink: 'https://tools.ietf.org/html/rfc3339',
47 },
48 time: {
49 label: i18n`time`,
50 text: i18n`the string must be a time string, according to `,
51 specname: 'RFC 3339, section 5.6',
52 speclink: 'https://tools.ietf.org/html/rfc3339',
53 },
54 duration: {
55 label: i18n`duration`,
56 text: i18n`the string must be a duration string, according to `,
57 specname: 'RFC 3339, section 5.6',
58 speclink: 'https://tools.ietf.org/html/rfc3339',
59 },
60 email: {
61 label: i18n`email`,
62 text: i18n`the string must be an email address, according to `,
63 specname: 'RFC 5322, section 3.4.1',
64 speclink: 'https://tools.ietf.org/html/rfc5322',
65 },
66 'idn-email': {
67 label: i18n`(international) email`,
68 text: i18n`the string must be an (international) email address, according to `,
69 specname: 'RFC 6531',
70 speclink: 'https://tools.ietf.org/html/rfc6531',
71 },
72 hostname: {
73 label: i18n`hostname`,
74 text: i18n`the string must be a hostname, according to `,
75 specname: 'RFC 1123, section 2.1',
76 speclink: 'https://tools.ietf.org/html/rfc1123',
77 },
78 'idn-hostname': {
79 label: i18n`(international) hostname`,
80 text: i18n`the string must be an (IDN) hostname, according to `,
81 specname: 'RFC 5890, section 2.3.2.3',
82 speclink: 'https://tools.ietf.org/html/rfc5890',
83 },
84 ipv4: {
85 label: i18n`IPv4`,
86 text: i18n`the string must be an IPv4 address (dotted quad), according to `,
87 specname: 'RFC 2673, section 3.2',
88 speclink: 'https://tools.ietf.org/html/rfc2673',
89 },
90 ipv6: {
91 label: i18n`IPv6`,
92 text: i18n`the string must be an IPv6 address, according to `,
93 specname: 'RFC 4291, section 2.2',
94 speclink: 'https://tools.ietf.org/html/rfc4291',
95 },
96 uri: {
97 label: i18n`URI`,
98 text: i18n`the string must be a URI, according to `,
99 specname: 'RFC 3986',
100 speclink: 'https://tools.ietf.org/html/rfc3986',
101 },
102 iri: {
103 label: i18n`IRI`,
104 text: i18n`the string must be a IRI, according to `,
105 specname: 'RFC 3987',
106 speclink: 'https://tools.ietf.org/html/rfc3987',
107 },
108 'uri-reference': {
109 label: i18n`URI reference`,
110 text: i18n`the string must be a URI reference, according to `,
111 specname: 'RFC 3986',
112 speclink: 'https://tools.ietf.org/html/rfc3986',
113 },
114 'iri-reference': {
115 label: i18n`IRI reference`,
116 text: i18n`the string must be a IRI reference, according to `,
117 specname: 'RFC 3987',
118 speclink: 'https://tools.ietf.org/html/rfc3987',
119 },
120 uuid: {
121 label: i18n`UUID`,
122 text: i18n`the string must be a UUID, according to `,
123 specname: 'RFC 4122',
124 speclink: 'https://tools.ietf.org/html/rfc4122',
125 },
126 'json-pointer': {
127 label: i18n`JSON Pointer`,
128 text: i18n`the string must be a JSON Pointer, according to `,
129 specname: 'RFC 6901, section 5',
130 speclink: 'https://tools.ietf.org/html/rfc6901',
131 },
132 'relative-json-pointer': {
133 label: i18n`Relative JSON Pointer`,
134 text: i18n`the string must be a relative JSON Pointer, according to `,
135 specname: 'draft-handrews-relative-json-pointer-01',
136 speclink: 'https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01',
137 },
138 regex: {
139 label: i18n`RegEx`,
140 text: i18n`the string must be a regular expression, according to `,
141 specname: 'ECMA-262',
142 speclink: 'http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf',
143 },
144 'uri-template': {
145 label: i18n`URI Template`,
146 text: i18n`the string must be a URI template, according to `,
147 specname: 'RFC 6570',
148 speclink: 'https://tools.ietf.org/html/rfc6570',
149 },
150 };
151
152 const headerprops = [
153 /*
154 {
155 name: 'type',
156 title: i18n`Type`,
157 objectlabel: i18n`Object`,
158 arraylabel: i18n`Array`,
159 multiplelabel: i18n`Multiple`,
160 mergedlabel: i18n`Merged`,
161 undefinedlabel: i18n`Undefined`,
162 numberlabel: i18n`Number`,
163 booleanlabel: i18n`Boolean`,
164 stringlabel: i18n`String`,
165 integerlabel: i18n`Integer`,
166 nulllabel: i18n`Null`,
167 },
168 */
169 {
170 name: 'abstract',
171 title: i18n`Abstract`,
172 truelabel: i18n`Cannot be instantiated`,
173 falselabel: i18n`Can be instantiated`,
174 undefinedlabel: i18n`Unknown abstraction`,
175 },
176 {
177 name: 'extensible',
178 title: i18n`Extensible`,
179 undefinedlable: i18n`Unknown extensibility`,
180 truelabel: i18n`Yes`,
181 falselabel: i18n`No`,
182 },
183 {
184 name: 'status',
185 title: i18n`Status`,
186 undefinedlabel: 'Unknown status',
187 deprecatedlabel: i18n`Deprecated`,
188 stablelabel: i18n`Stable`,
189 stabilizinglabel: i18n`Stabilizing`,
190 experimentallabel: i18n`Experimental`,
191 },
192 {
193 name: 'identifiable',
194 title: i18n`Identifiable`,
195 truelabel: i18n`Yes`,
196 falselabel: i18n`No`,
197 undefinedlabel: i18n`Unknown identifiability`,
198 },
199 {
200 name: 'custom',
201 title: i18n`Custom Properties`,
202 truelabel: i18n`Allowed`,
203 falselabel: i18n`Forbidden`,
204 undefinedlabel: i18n`Unknown custom properties`,
205 },
206 {
207 name: 'additional',
208 title: i18n`Additional Properties`,
209 truelabel: i18n`Allowed`,
210 falselabel: i18n`Forbidden`,
211 undefinedlabel: i18n`Unknown additional properties`,
212 },
213 {
214 name: 'restrictions',
215 title: i18n`Access Restrictions`,
216 readOnlylabel: i18n`Read only`,
217 writeOnlylabel: i18n`Write only`,
218 secretlabel: i18n`cannot be read or written`,
219 undefinedlabel: i18n`none`,
220 },
221 {
222 name: 'definedin',
223 title: i18n`Defined In`,
224 undefinedlabel: i18n`Unknown definition`,
225 },
226 ];
227
228 function makecomment(schema) {
229 if (schema[keyword`$comment`]) {
230 return [
231 blockquote(schema[s.meta].longcomment),
232 ];
233 }
234 return [];
235 }
236
237 /**
238 * Generates the overall header for the schema documentation
239 * @param {*} schema
240 */
241 function makeheader(schema) {
242 // console.log('making header for', schema[s.filename], schema[s.pointer]);
243 if (header) {
244 return [
245 heading(1, text(i18n`${gentitle(schema[s.titles], schema[keyword`type`])} Schema`)),
246 paragraph(code('txt', schema[s.id] + (schema[s.pointer] ? `#${schema[s.pointer]}` : ''))),
247 schema[s.meta].longdescription,
248 ...makecomment(schema),
249 table('left', [
250 // iterate over header
251 tableRow(
252 flist(
253 map(headerprops,
254 ({ name, title }) => {
255 if (links[name]) {
256 return tableCell(link(links[name], i18n`What does ${title} mean?`, text(title)));
257 }
258 return tableCell(text(title));
259 }), Array,
260 ),
261 ),
262 tableRow(
263 flist(
264 map(headerprops,
265 (prop) => {
266 // this is a linked property
267 if (schema[s.meta]
268 && typeof schema[s.meta][prop.name] === 'object'
269 && schema[s.meta][prop.name].link
270 && schema[s.meta][prop.name].text) {
271 return tableCell(link(rewritelinks(schema[s.meta][prop.name].link), i18n`open original schema`, [text(schema[s.meta][prop.name].text)]));
272 }
273 const value = schema[s.meta] ? schema[s.meta][prop.name] : undefined;
274 return tableCell(text(prop[`${String(value)}label`] || i18n`Unknown`));
275 }), Array,
276 ),
277 ),
278 ]),
279 ];
280 }
281 return [];
282 }
283
284 function type(property) {
285 if (!Array.isArray(property[keyword`type`]) && typeof property[keyword`type`] === 'object') {
286 return text(i18n`Unknown Type`);
287 }
288 const types = Array.isArray(property[keyword`type`]) ? property[keyword`type`] : [property[keyword`type`]];
289 const realtypes = flist(filter(types, (mytype) => mytype !== 'null' && mytype !== undefined));
290 if (property[keyword`allOf`] || property[keyword`anyOf`] || property[keyword`oneOf`] || property[keyword`not`]) {
291 return text(i18n`Merged`);
292 } else if (size(realtypes) === 0) {
293 return text(i18n`Not specified`);
294 }
295 return (size(realtypes) === 1) ? inlineCode(realtypes[0]) : text(i18n`Multiple`);
296 }
297
298 function nullable(property) {
299 const types = Array.isArray(property[keyword`type`]) ? property[keyword`type`] : [property[keyword`type`]];
300 const nulltypes = flist(filter(types, (mytype) => mytype === keyword`null`));
301 if (size(nulltypes)) {
302 return text(i18n`can be null`);
303 }
304 return text(i18n`cannot be null`);
305 }
306
307 /**
308 * Generates the overview table row for a single property definition
309 * @param {*} param0
310 */
311 function makepropheader(required = [], ispattern = false, slugger) {
312 return ([name, definition]) => tableRow([
313 tableCell(ispattern ? inlineCode(name) : link(`#${slugger.slug(name)}`, '', text(name))), // Property
314 tableCell(type(definition)),
315 tableCell(text(required.indexOf(name) > -1 ? i18n`Required` : i18n`Optional`)),
316 tableCell(nullable(definition)),
317 tableCell(link(`${definition[s.slug]}.md`, `${definition[s.id]}#${definition[s.pointer]}`,
318 text(definition[s.titles] && definition[s.titles][0] ? definition[s.titles][0] : i18n`Untitled schema`))),
319 ]);
320 }
321
322 /**
323 * Generates the table of contents for a properties
324 * object.
325 * @param {*} props
326 */
327 function makeproptable(props = {}, patternProps = {}, additionalProps, required, slugger) {
328 if (skipproperties.includes('proptable')) {
329 return paragraph();
330 }
331 const proprows = Object
332 .entries(props)
333 .map(makepropheader(required, false, slugger));
334 const patternproprows = Object
335 .entries(patternProps)
336 .map(makepropheader(required, true, slugger));
337 const additionalproprows = (() => {
338 if (additionalProps) {
339 const any = additionalProps === true;
340 return [tableRow([
341 tableCell(text(i18n`Additional Properties`)),
342 tableCell(any ? text('Any') : type(additionalProps)),
343 tableCell(text(i18n`Optional`)),
344 tableCell(any ? text('can be null') : nullable(additionalProps)),
345 tableCell(any ? text('') : link(`${additionalProps[s.slug]}.md`, `${additionalProps[s.id]}#${additionalProps[s.pointer]}`, text(additionalProps[s.titles][0] || i18n`Untitled schema`))),
346 ])];
347 }
348 return [];
349 })();
350
351 // const proprows = flist(map(iter(props || {}), makepropheader(required)));
352
353 return table('left', [
354 tableRow([
355 tableCell(text(i18n`Property`)),
356 tableCell(text(i18n`Type`)),
357 tableCell(text(i18n`Required`)),
358 tableCell(text(i18n`Nullable`)),
359 tableCell(text(i18n`Defined by`)),
360 ]),
361 ...proprows,
362 ...patternproprows,
363 ...additionalproprows,
364 ]);
365 }
366
367 function makearrayfact(items, additional) {
368 if (skipproperties.includes('arrayfact')) {
369 return '';
370 }
371 return listItem([
372 paragraph([text(i18n`Type: `), text(i18n`an array where each item follows the corresponding schema in the following list:`)]),
373 list('ordered',
374 [...items.map((schema) => listItem(paragraph(link(
375 `${schema[s.slug]}.md`,
376 i18n`check type definition`,
377 text(gentitle(schema[s.titles], schema[keyword`type`])),
378 )))),
379 ...(() => {
380 if (additional === true) {
381 return [listItem(paragraph(text(i18n`and all following items may follow any schema`)))];
382 } else if (typeof additional === 'object') {
383 return [listItem(paragraph([text(i18n`and all following items must follow the schema: `),
384 link(
385 `${additional[s.slug]}.md`,
386 i18n`check type definition`,
387 text(gentitle(additional[s.titles], additional[keyword`type`])),
388 )]))];
389 }
390 return [];
391 })(),
392 ])]);
393 }
394
395 function maketypefact(definition, isarray = '') {
396 const alltypes = Array.isArray(definition[keyword`type`]) ? definition[keyword`type`] : [definition[keyword`type`]];
397 // filter out types that are null
398 const realtypes = alltypes.filter((mytype) => mytype !== keyword`null`);
399 // can the type be `null`
400 const isnullable = alltypes.filter((mytype) => mytype === keyword`null`).length > 0;
401 // is there only a single type or can there be multiple types
402 const singletype = realtypes.length <= 1;
403 const [firsttype] = realtypes;
404 // is `null` the only allowed value
405 const nulltype = isnullable && realtypes.length === 0;
406
407 const array = firsttype === keyword`array`;
408 const merged = !!(definition[keyword`allOf`] || definition[keyword`anyOf`] || definition[keyword`oneOf`] || definition[keyword`not`]);
409
410 if (array && Array.isArray(definition[keyword`items`])) {
411 return makearrayfact(definition[keyword`items`], definition[keyword`additionalItems`]);
412 } else if (array && definition[keyword`items`]) {
413 return maketypefact(definition[keyword`items`], `${isarray}[]`);
414 }
415
416 const typefact = (() => {
417 if (nulltype) {
418 return [inlineCode(`null${isarray}`), text(i18n`, the value must be null`)];
419 } else if (singletype && firsttype && typeof firsttype === 'string') {
420 return [inlineCode(firsttype + isarray)];
421 } else if (!singletype) {
422 return [text(isarray ? i18n`an array of the following:` : i18n`any of the folllowing: `), ...flist(flat(realtypes.map((mytype, index) => [inlineCode(mytype || i18n`not defined`), text(index === realtypes.length - 1 ? '' : i18n` or `)])))];
423 } else if (merged) {
424 return [text(isarray ? 'an array of merged types' : i18n`merged type`)];
425 }
426 // console.log('unknown type', realtypes, singletype, merged, definition[s.pointer]);
427 return [text(i18n`unknown` + isarray)];
428 })();
429
430 const typelink = (() => {
431 if (definition[keyword`title`] && typeof definition[keyword`title`] === 'string') {
432 // if the type has a title, always create a link to the schema
433 return [text(' ('), link(`${definition[s.slug]}.md`, '', text(definition[keyword`title`])), text(')')];
434 } else if (!singletype || firsttype === keyword`object` || merged) {
435 return [text(' ('), link(`${definition[s.slug]}.md`, '', text(i18n`Details`)), text(')')];
436 }
437 return [];
438 })();
439 const retval = listItem(paragraph([text(i18n`Type: `), ...typefact, ...typelink]));
440 return retval;
441 }
442
443 function makenullablefact(definition) {
444 const alltypes = Array.isArray(definition[keyword`type`]) ? definition[keyword`type`] : [definition[keyword`type`]];
445 // can the type be `null`
446 const isnullable = alltypes.filter((mytype) => mytype === keyword`null`).length > 0;
447
448 if (isnullable) {
449 return listItem(paragraph(text(i18n`can be null`)));
450 } else {
451 return listItem(paragraph(text(i18n`cannot be null`)));
452 }
453 }
454
455 function makedefinedinfact(definition) {
456 return listItem(paragraph([
457 text(i18n`defined in: `),
458 link(`${definition[s.slug]}.md`, `${definition[s.id]}#${definition[s.pointer]}`, text(definition[s.titles] && definition[s.titles][0] ? definition[s.titles][0] : i18n`Untitled schema`)),
459 ]));
460 }
461
462 function makefactlist(name, definition, required = []) {
463 const children = [];
464
465 if (required.indexOf(name) > -1) {
466 children.push(listItem(text(i18n`is required`)));
467 } else {
468 children.push(listItem(text(i18n`is optional`)));
469 }
470
471 if (!skipproperties.includes('typefact')) {
472 children.push(maketypefact(definition));
473 }
474 if (!skipproperties.includes('nullablefact')) {
475 children.push(makenullablefact(definition));
476 }
477 if (!skipproperties.includes('definedinfact')) {
478 children.push(makedefinedinfact(definition));
479 }
480
481 const additionalfacts = includeproperties.map((propname) => {
482 if (definition[propname]) {
483 return listItem(text(`${propname}: ${String(definition[propname])}`));
484 }
485 return undefined;
486 }).filter((item) => item !== undefined);
487
488 children.push(...additionalfacts);
489
490 return list('unordered', children);
491 }
492
493 function simpletitle(schema) {
494 return schema[s.parent] ? schema[s.pointer].split('/').pop() : gentitle(schema[s.titles], schema[keyword`type`]);
495 }
496
497 function makejointypelist(schema, depth = 0, maxdepth = 3) {
498 if (schema[keyword`oneOf`] && depth <= maxdepth) {
499 return [
500 paragraph(text(i18n`one (and only one) of`)),
501 list('unordered', [
502 ...schema[keyword`oneOf`].map((subschema) => listItem(makejointypelist(subschema, depth + 1))),
503 ]),
504 ];
505 } else if (schema[keyword`anyOf`] && depth <= maxdepth) {
506 return [
507 paragraph(text(i18n`any of`)),
508 list('unordered', [
509 ...schema[keyword`anyOf`].map((subschema) => listItem(makejointypelist(subschema, depth + 1))),
510 ]),
511 ];
512 } else if (schema[keyword`allOf`] && depth <= maxdepth) {
513 return [
514 paragraph(text(i18n`all of`)),
515 list('unordered', [
516 ...schema[keyword`allOf`].map((subschema) => listItem(makejointypelist(subschema, depth + 1))),
517 ]),
518 ];
519 } else if (schema[keyword`not`] && depth <= maxdepth) {
520 const subschema = schema[keyword`not`];
521 return [
522 paragraph(text(i18n`not`)),
523 list('unordered', [
524 listItem(makejointypelist(subschema, depth + 1)),
525 ]),
526 ];
527 } else if (depth > 0) {
528 return [
529 link(`${schema[s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[s.titles], schema[keyword`type`]))),
530 ];
531 } else {
532 return [];
533 }
534 }
535
536 function maketypesection(schema, level = 1) {
537 if (skipproperties.includes('typesection')) {
538 return '';
539 }
540 const { children } = maketypefact(schema);
541 children[0].children.shift();
542 return [
543 heading(level + 1, text(i18n`${simpletitle(schema)} Type`)),
544 ...children,
545 ...makejointypelist(schema),
546 ];
547 }
548
549 function makeconstraintssection(schema, level = 1) {
550 const constraints = [];
551 if (schema[keyword`const`] !== undefined) {
552 constraints.push(paragraph([strong(text(i18n`constant`)), text(': '), text(i18n`the value of this property must be equal to:`)]));
553 constraints.push(code('json', JSON.stringify(schema[keyword`const`], undefined, 2)));
554 }
555
556 if (schema[keyword`enum`]) {
557 const metas = schema[keyword`meta:enum`] || {};
558 constraints.push(paragraph([strong(text(i18n`enum`)), text(': '), text(i18n`the value of this property must be equal to one of the following values:`)]));
559 constraints.push(table('left', [
560 tableRow([
561 tableCell(text(i18n`Value`)),
562 tableCell(text(i18n`Explanation`)),
563 ]),
564 ...schema[keyword`enum`].map((value) => tableRow([
565 tableCell(inlineCode(JSON.stringify(value))),
566 tableCell(text(metas[Array.isArray(value) ? JSON.stringify(value) : value] || '')),
567 ])),
568 ]));
569 }
570
571 // https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.2
572 if (schema[keyword`multipleOf`] !== undefined && typeof schema[keyword`multipleOf`] === 'number') {
573 // console.log('multiple!', schema[s.filename], schema[s.pointer]);
574 constraints.push(paragraph([strong(text(i18n`multiple of`)), text(': '), text(i18n`the value of this number must be a multiple of: `), inlineCode(String(schema[keyword`multipleOf`]))]));
575 }
576 if (schema[keyword`maximum`] !== undefined && typeof schema[keyword`maximum`] === 'number') {
577 // console.log('maximum!', schema[s.filename], schema[s.pointer]);
578 constraints.push(paragraph([strong(text(i18n`maximum`)), text(': '), text(i18n`the value of this number must smaller than or equal to: `), inlineCode(String(schema[keyword`maximum`]))]));
579 }
580 if (schema[keyword`exclusiveMaximum`] !== undefined && typeof schema[keyword`exclusiveMaximum`] === 'number') {
581 // console.log('exclusiveMaximum!', schema[s.filename], schema[s.pointer]);
582 constraints.push(paragraph([strong(text(i18n`maximum (exclusive)`)), text(': '), text(i18n`the value of this number must be smaller than: `), inlineCode(String(schema[keyword`exclusiveMaximum`]))]));
583 }
584 if (schema[keyword`minimum`] !== undefined && typeof schema[keyword`minimum`] === 'number') {
585 // console.log('minimum!', schema[s.filename], schema[s.pointer]);
586 constraints.push(paragraph([strong(text(i18n`minimum`)), text(': '), text(i18n`the value of this number must greater than or equal to: `), inlineCode(String(schema[keyword`minimum`]))]));
587 }
588 if (schema[keyword`exclusiveMinimum`] !== undefined && typeof schema[keyword`exclusiveMinimum`] === 'number') {
589 // console.log('exclusiveMinimum!', schema[s.filename], schema[s.pointer]);
590 constraints.push(paragraph([strong(text(i18n`minimum (exclusive)`)), text(': '), text(i18n`the value of this number must be greater than: `), inlineCode(String(schema[keyword`exclusiveMinimum`]))]));
591 }
592
593 // https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.3
594 if (schema[keyword`maxLength`] !== undefined && typeof schema[keyword`maxLength`] === 'number') {
595 // console.log('maxLength!', schema[s.filename], schema[s.pointer]);
596 constraints.push(paragraph([strong(text(i18n`maximum length`)), text(': '), text(i18n`the maximum number of characters for this string is: `), inlineCode(String(schema[keyword`maxLength`]))]));
597 }
598 if (schema[keyword`minLength`] !== undefined && typeof schema[keyword`minLength`] === 'number') {
599 // console.log('minLength!', schema[s.filename], schema[s.pointer]);
600 constraints.push(paragraph([strong(text(i18n`minimum length`)), text(': '), text(i18n`the minimum number of characters for this string is: `), inlineCode(String(schema[keyword`minLength`]))]));
601 }
602 if (schema[keyword`pattern`]) {
603 // console.log('pattern!', schema[s.filename], schema[s.pointer]);
604 constraints.push(paragraph([strong(text(i18n`pattern`)), text(': '), text(i18n`the string must match the following regular expression: `)]));
605 constraints.push(code('regexp', schema[keyword`pattern`]));
606 constraints.push(paragraph([link(`https://regexr.com/?expression=${encodeURIComponent(schema[keyword`pattern`])}`, i18n`try regular expression with regexr.com`, text(i18n`try pattern`))]));
607 }
608 // https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.7.3
609 if (schema.format && typeof schema.format === 'string' && formats[schema.format]) {
610 constraints.push(paragraph([
611 strong(text(formats[keyword([schema.format])].label)),
612 text(': '),
613 text(formats[schema.format].text),
614 link(formats[schema.format].speclink, i18n`check the specification`, text(formats[schema.format].specname)),
615 ]));
616 } else if (schema.format && typeof schema.format === 'string') {
617 constraints.push(paragraph([strong(text(i18n`unknown format`)), text(': '), text(i18n`the value of this string must follow the format: `), inlineCode(String(schema.format))]));
618 }
619
620 if (schema[keyword`contentEncoding`]) {
621 constraints.push(paragraph([strong(text(i18n`encoding`)), text(': '), text(i18n`the string content must be using the ${schema[keyword`contentEncoding`]} content encoding.`)]));
622 }
623 if (schema[keyword`contentMediaType`]) {
624 constraints.push(paragraph([strong(text(i18n`media type`)), text(': '), text(i18n`the media type of the contents of this string is: `), inlineCode(String(schema[keyword`contentMediaType`]))]));
625 }
626 if (schema[keyword`contentSchema`]) {
627 constraints.push(paragraph([
628 strong(text(i18n`schema`)), text(': '),
629 text(i18n`the contents of this string should follow this schema: `),
630 link(`${schema[keyword`contentSchema`][s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[keyword`contentSchema`][s.titles], schema[keyword`contentSchema`][keyword`type`])))]));
631 }
632
633 // https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.4
634 if (schema[keyword`maxItems`] !== undefined) {
635 // console.log('maxItems!', schema[s.filename], schema[s.pointer]);
636 constraints.push(paragraph([strong(text(i18n`maximum number of items`)), text(': '), text(i18n`the maximum number of items for this array is: `), inlineCode(String(schema[keyword`maxItems`]))]));
637 }
638 if (schema[keyword`minItems`] !== undefined) {
639 // console.log('minItems!', schema[s.filename], schema[s.pointer]);
640 constraints.push(paragraph([strong(text(i18n`minimum number of items`)), text(': '), text(i18n`the minimum number of items for this array is: `), inlineCode(String(schema[keyword`minItems`]))]));
641 }
642 if (schema[keyword`uniqueItems`]) {
643 // console.log('uniqueItems!', schema[s.filename], schema[s.pointer]);
644 constraints.push(paragraph([strong(text(i18n`unique items`)), text(': '), text(i18n`all items in this array must be unique. Duplicates are not allowed.`)]));
645 }
646 if (schema[keyword`minContains`] !== undefined && schema[keyword`contains`]) {
647 // console.log('minContains!', schema[s.filename], schema[s.pointer]);
648 constraints.push(paragraph([strong(text(i18n`minimum number of contained items`)), text(': '), text(`${i18n`this array may not contain fewer than ${schema[keyword`minContains`]} items that validate against the schema:`} `),
649 link(`${schema[keyword`contains`][s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[keyword`contains`][s.titles], schema[keyword`contains`][keyword`type`])))]));
650 }
651 if (schema[keyword`maxContains`] !== undefined && schema[keyword`contains`]) {
652 // console.log('maxContains!', schema[s.filename], schema[s.pointer]);
653 constraints.push(paragraph([strong(text(i18n`maximum number of contained items`)), text(': '), text(`${i18n`this array may not contain more than ${schema[keyword`maxContains`]} items that validate against the schema:`} `),
654 link(`${schema[keyword`contains`][s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[keyword`contains`][s.titles], schema[keyword`contains`][keyword`type`])))]));
655 }
656
657 // https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.5
658 if (schema[keyword`maxProperties`] !== undefined) {
659 // console.log('maxProperties!', schema[s.filename], schema[s.pointer]);
660 constraints.push(paragraph([strong(text(i18n`maximum number of properties`)), text(': '), text(i18n`the maximum number of properties for this object is: `), inlineCode(String(schema[keyword`maxProperties`]))]));
661 }
662 if (schema[keyword`minProperties`] !== undefined) {
663 // console.log('minProperties!', schema[s.filename], schema[s.pointer]);
664 constraints.push(paragraph([strong(text(i18n`minimum number of properties`)), text(': '), text(i18n`the minimum number of properties for this object is: `), inlineCode(String(schema[keyword`minProperties`]))]));
665 }
666
667 if (constraints.length > 0) {
668 return [heading(level + 1, text(i18n`${simpletitle(schema)} Constraints`)), ...constraints];
669 }
670 return [];
671 }
672
673 function makeexamples(schema, level = 1) {
674 if (schema[keyword`examples`] && schema[keyword`examples`].length > 0 && exampleformat === 'yaml') {
675 return [
676 heading(level + 1, text(i18n`${simpletitle(schema)} Examples`)),
677 ...schema[keyword`examples`].map((example) => paragraph(code('yaml', yaml.dump(example, undefined, 2)))),
678 ];
679 }
680 if (schema[keyword`examples`] && schema[keyword`examples`].length > 0 && exampleformat === 'json') {
681 return [
682 heading(level + 1, text(i18n`${simpletitle(schema)} Examples`)),
683 ...schema[keyword`examples`].map((example) => paragraph(code('json', JSON.stringify(example, undefined, 2)))),
684 ];
685 }
686 return [];
687 }
688
689 function makedefault(schema, level = 1) {
690 if (schema[keyword`default`]) {
691 return [
692 heading(level + 1, text(i18n`${simpletitle(schema)} Default Value`)),
693 paragraph(text(i18n`The default value is:`)),
694 paragraph(code('json', JSON.stringify(schema[keyword`default`], undefined, 2))),
695 ];
696 }
697 return [];
698 }
699
700 function makerestrictions(schema, level = 1) {
701 if (schema[keyword`readOnly`] && schema[keyword`writeOnly`]) {
702 return [
703 heading(level + 1, text(i18n`${simpletitle(schema)} Access Restrictions`)),
704 paragraph(text(i18n`The value of this property is managed exclusively by the owning authority and never exposed to the outside. It can neither be read nor written.`)),
705 ];
706 }
707 if (schema[keyword`readOnly`]) {
708 return [
709 heading(level + 1, text(i18n`${simpletitle(schema)} Access Restrictions`)),
710 paragraph(text(i18n`The value of this property is managed exclusively by the owning authority, and attempts by an application to modify the value of this property are expected to be ignored or rejected by that owning authority`)),
711 ];
712 }
713 if (schema[keyword`writeOnly`]) {
714 return [
715 heading(level + 1, text(i18n`${simpletitle(schema)} Access Restrictions`)),
716 paragraph(text(i18n`The value of this property is never present when the instance is retrieved from the owning authority. It can be present when sent to the owning authority to update or create the document (or the resource it represents), but it will not be included in any updated or newly created version of the instance.`)),
717 ];
718 }
719 return [];
720 }
721
722 function makeproplist(properties = {},
723 patternProperties = {},
724 additionalProperties,
725 required,
726 level = 2) {
727 return [
728 ...flist(flat(Object.entries(properties || {}).map(([name, definition]) => {
729 const description = definition[s.meta] && definition[s.meta].longdescription ? definition[s.meta].longdescription : paragraph(text(i18n`no description`));
730
731 return [
732 heading(level + 1, text(name)),
733 description,
734 ...makecomment(definition),
735 paragraph(inlineCode(name)),
736 makefactlist(name, definition, required),
737 ...maketypesection(definition, level + 1),
738 ...makeconstraintssection(definition, level + 1),
739 ...makedefault(definition, level + 1),
740 ...makeexamples(definition, level + 1),
741 ...makerestrictions(definition, level + 1),
742 ];
743 }))),
744 ...flist(flat(Object.entries(patternProperties || {}).map(([name, definition]) => {
745 const description = definition[s.meta].longdescription || paragraph(text(i18n`no description`));
746
747 return [
748 heading(level + 1, [text(i18n`Pattern: `), inlineCode(name)]),
749 description,
750 ...makecomment(definition),
751 paragraph(inlineCode(name)),
752 makefactlist(name, definition, required),
753 ...maketypesection(definition, level + 1),
754 ...makeconstraintssection(definition, level + 1),
755 ...makedefault(definition, level + 1),
756 ...makeexamples(definition, level + 1),
757 ...makerestrictions(definition, level + 1),
758 ];
759 }))),
760 ...((definition) => {
761 if (typeof additionalProperties === 'object') {
762 const description = definition[s.meta].longdescription || paragraph(text(i18n`no description`));
763 return [
764 heading(level + 1, text(i18n`Additional Properties`)),
765 paragraph(text(i18n`Additional properties are allowed, as long as they follow this schema:`)),
766 description,
767 ...makecomment(definition),
768 makefactlist(i18n`Additional properties`, definition, required),
769 ...maketypesection(definition, level + 1),
770 ...makeconstraintssection(definition, level + 1),
771 ...makedefault(definition, level + 1),
772 ...makeexamples(definition, level + 1),
773 ...makerestrictions(definition, level + 1),
774 ];
775 } else if (additionalProperties === true) {
776 return [
777 heading(level + 1, text(i18n`Additional Properties`)),
778 paragraph(text(i18n`Additional properties are allowed and do not have to follow a specific schema`)),
779 ];
780 }
781 // nothing
782 return [];
783 })(additionalProperties),
784 ];
785 }
786
787 /**
788 * Generates the definitions section for a schema
789 * @param {*} schema
790 */
791 function makedefinitions(schema, slugger) {
792 if (schema.definitions || schema[keyword`$defs`]) {
793 const defgroups = [
794 ...Object.entries(schema[keyword`$defs`] || {}),
795 ...Object.entries(schema.definitions || {})]
796 .map(([groupname, subschema]) => {
797 const grouptable = makeproptable(
798 subschema[keyword`properties`],
799 subschema[keyword`patternProperties`],
800 subschema[keyword`additionalProperties`],
801 subschema[keyword`required`],
802 slugger,
803 );
804 return [
805 heading(2, text(i18n`Definitions group ${groupname}`)),
806 paragraph(text(i18n`Reference this group by using`)),
807 code('json', JSON.stringify({ $ref: `${subschema[s.id]}#${subschema[s.pointer]}` })),
808 grouptable,
809 ...makeproplist(
810 subschema[keyword`properties`],
811 subschema[keyword`patternProperties`],
812 subschema[keyword`additionalProperties`],
813 subschema[keyword`required`],
814 2,
815 ),
816 ];
817 });
818
819 return [
820 heading(1, text(i18n`${gentitle(schema[s.titles], schema[keyword`type`])} Definitions`)),
821 ...flist(flat(defgroups)),
822 ];
823 }
824 return [];
825 }
826
827 /**
828 * Generates the properties section for a schema
829 * @param {*} schema
830 */
831 function makeproperties(schema, slugger) {
832 if (schema[keyword`properties`] || schema[keyword`patternProperties`] || schema[keyword`additionalProperties`]) {
833 return [
834 heading(1, text(i18n`${schema.title} Properties`)),
835 makeproptable(
836 schema[keyword`properties`],
837 schema[keyword`patternProperties`],
838 schema[keyword`additionalProperties`],
839 schema[keyword`required`],
840 slugger,
841 ),
842 ...makeproplist(
843 schema[keyword`properties`],
844 schema[keyword`patternProperties`],
845 schema[keyword`additionalProperties`],
846 schema[keyword`required`],
847 1,
848 ),
849 ];
850 }
851 return [];
852 }
853
854 console.log('generating markdown');
855 return (schemas) => foldl(schemas, {}, (pv, schema) => {
856 const slugger = ghslugger();
857 // eslint-disable-next-line no-param-reassign
858 pv[schema[s.slug]] = root([
859 // todo add more elements
860 ...makeheader(schema),
861 ...maketypesection(schema, 1),
862 ...makeconstraintssection(schema, 1),
863 ...makedefault(schema, 1),
864 ...makeexamples(schema, 1),
865 ...makeproperties(schema, slugger),
866 ...makedefinitions(schema, slugger),
867 ]);
868 return pv;
869 });
870}
871
872module.exports = build;