@adobe/jsonschema2md
Version:
Validate and document complex JSON Schemas the easy way.
342 lines (305 loc) • 10.8 kB
JavaScript
/*
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
/* @ts-check */
import yargs from 'yargs';
import fs from 'fs-extra';
import { readdirpPromise } from 'readdirp';
import {
iter, pipe, filter, map, obj,
} from 'ferrum';
import nodepath from 'path';
import { i18nConfig } from 'es2015-i18n-tag';
import { fileURLToPath } from 'url';
import traverse from './traverseSchema.js';
import build from './markdownBuilder.js';
import { writereadme, writemarkdown } from './writeMarkdown.js';
import readme from './readmeBuilder.js';
import loader from './schemaProxy.js';
import writeSchema from './writeSchema.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = nodepath.dirname(__filename);
const { debug, info, error } = console;
/**
* @typedef {import("../types/api").JsonSchema} JsonSchema
*/
/**
* @typedef {import("../types/api").SchemaList} SchemaList
*/
/**
* @typedef {import("../types/api").SchemaContent} SchemaContent
*/
/**
* @typedef {import("../types/api").SchemaFiles} SchemaFiles
*/
/**
* @typedef {import("../types/api").GeneratedOutput} GeneratedOutput
*/
/**
* Public API for jsonschema2md that can be used to turn JSON Schema files
* into readable Markdown documentation.
* @param {JsonSchema | SchemaFiles} schema JSON Schema input to get data from
* @param {Object} options Additional options for generation
* @param {string} [options.schemaPath] - (optional) Path to directory containing all JSON Schemas
* or a single JSON Schema file. This will be considered as the baseURL.
* @param {string} [options.outDir] - (optional) Path to output directory. Generating files will
* be skipped if directory is not specified.
* @param {{ [key:string]: string }} [options.metadata] - (optional) Add metadata elements to
* .md files.
* @param {string} [options.schemaOut] - (optional) Output JSON Schema files including
* description and validated examples in the specified folder.
* @param {boolean} [options.includeReadme=true] - (optional) Generate a README.md file in the
* output directory.
* @param {{ [key:string]: string }[]} [options.links] - (optional) Add this file as a link
* explaining the specified attribute.
* @param {string} [options.i18n="locales/"] - (optional) Path to a locales folder with
* JSON files used for internationalization.
* @param {"en_US" | "de"} [options.language="en_US"] - (optional) Selected language.
* @param {"json" | "yaml"} [options.exampleFormat="json"] - (optional) How to format examples.
* @param {string[]} [options.includeProperties=[]] - (optional) Name of custom properties
* which should be also in the description of an element.
* @param {boolean} [options.header=true] - (optional) Whether or not to include the header
* in markdown.
* @param {string[]} [options.skipProperties=[]] - (optional) Name of a default property to
* skip in markdown.
* @returns {GeneratedOutput} List of raw markdown that were generated from input schema.
*/
export function jsonschema2md(schema, options) {
const {
schemaPath,
outDir,
metadata,
schemaOut,
includeReadme,
links,
i18n,
language,
exampleFormat,
includeProperties,
header,
skipProperties,
} = options;
if (!schema || typeof schema !== 'object') {
throw Error('Input is not valid. Provide JSON schema either as Object or Array.');
}
const locales = i18n || nodepath.resolve(__dirname, 'locales');
i18nConfig(fs.readJSONSync(nodepath.resolve(locales, `${language || 'en_US'}.json`)));
let out = outDir;
if (options.out) {
// eslint-disable-next-line no-console
console.warn("Options 'out' has been deprecated. Please, use 'outDir' instead");
out = options.out;
}
let meta = metadata;
if (options.meta) {
// eslint-disable-next-line no-console
console.warn("Options 'meta' has been deprecated. Please, use 'metadata' instead");
meta = options.meta;
}
/** @type {SchemaFiles} */
let normalized;
if (Array.isArray(schema)) {
normalized = schema;
} else {
normalized = [{
fileName: 'definition.schema.json',
content: schema,
}];
}
const schemaLoader = loader();
// collect data about the schemas and turn everything into a big object
const schemas = pipe(
normalized,
// Checking if data contains the file path or its contents (JSON schema)
map(({ fileName, fullPath, content }) => {
if (!content && fullPath) {
return schemaLoader(fullPath, fs.readJSONSync(fullPath));
}
return schemaLoader(fileName, content);
}),
traverse,
);
/**
* @type {GeneratedOutput}
*/
const output = {};
console.log('preparing schemas...');
output.schema = writeSchema({
schemadir: schemaOut,
origindir: schemaPath,
})(schemas);
if (includeReadme) {
console.log('preparing README...');
output.readme = pipe(
schemas,
// build readme
readme({
readme: true,
}),
writereadme({
out,
info,
error,
debug,
meta,
}),
);
}
console.log('preparing documentation...');
output.markdown = pipe(
schemas,
// generate Markdown ASTs
build({
header,
links,
includeProperties,
exampleFormat,
skipProperties,
rewritelinks: (origin) => {
const mddir = out;
if (!mddir) {
return origin;
}
const srcdir = schemaPath;
const schemadir = schemaOut || schemaPath;
const target = nodepath.relative(
mddir,
nodepath.resolve(schemadir, nodepath.relative(srcdir, origin)),
).split(nodepath.sep).join(nodepath.posix.sep);
return target;
},
}),
// write to files
writemarkdown({
out,
info,
error,
debug,
meta,
}),
);
return output;
}
/**
* Main function used in the CLI.
* @param {{ [key:string]: unknown }} args CLI arguments from user input
* @returns The generated Markdown files to the specified output directory
*/
export async function main(args) {
// parse/process command line arguments
const { argv } = yargs(args)
.usage('Generate Markdown documentation from JSON Schema.\n\nUsage: $0')
.demand('d')
.alias('d', 'input')
.describe('d', 'path to directory containing all JSON Schemas to process. This will be considered as the baseURL. By default only files ending in .schema.json will be processed, unless the schema-extension is set with the -e flag.')
.coerce('d', (d) => {
const resolved = d && nodepath.resolve(d);
if (fs.existsSync(resolved) && fs.lstatSync(d).isDirectory()) {
return resolved;
}
throw new Error(`Input file "${d}" is not a directory!`);
})
.alias('o', 'out')
.describe('o', 'path to output directory')
.default('o', nodepath.resolve(nodepath.join('.', 'out')))
.coerce('o', (o) => nodepath.resolve(o))
.option('m', {
type: 'array',
})
.alias('m', 'meta')
.describe('m', 'add metadata elements to .md files Eg -m template=reference. Multiple values can be added by repeating the flag Eg: -m template=reference -m hide-nav=true')
.coerce('m', (m) => m && pipe(
// turn this into an object of key value pairs
iter(m),
map((i) => i.split('=')),
obj,
))
.alias('x', 'schema-out')
.describe('x', 'output JSON Schema files including description and validated examples in the specified folder, or suppress with -')
.default('x', nodepath.resolve(nodepath.join('.', 'out')))
.coerce('x', (x) => (x === '-' ? '-' : nodepath.resolve(x)))
.alias('e', 'schema-extension')
.describe('e', 'JSON Schema file extension eg. schema.json or json')
.default('e', 'schema.json')
.alias('n', 'no-readme')
.describe('n', 'Do not generate a README.md file in the output directory')
.describe('link-*', 'Add this file as a link the explain the * attribute, e.g. --link-abstract=abstract.md')
.alias('i', 'i18n')
.describe('i', 'path to a locales folder with JSON files')
.default('i', nodepath.resolve(__dirname, 'locales'))
.coerce('i', (i) => nodepath.resolve(i))
.alias('l', 'language')
.describe('l', 'the selected language')
.choices('l', ['en_US', 'de', 'nl_NL'])
.default('l', 'en_US')
.alias('f', 'example-format')
.describe('f', 'how to format examples')
.choices('f', ['json', 'yaml'])
.default('f', 'json')
.alias('p', 'properties')
.array('p')
.describe('p', 'name of a custom property which should be also in the description of an element (may be used multiple times)')
.default('p', [])
.alias('h', 'header')
.boolean('h')
.describe('h', 'if the value is false the header will be skipped')
.default('h', true)
.alias('s', 'skip')
.array('s')
.describe('s', 'name of a default property to skip in markdown (may be used multiple times), e.g. -s typefact -s proptable')
.default('s', []);
const links = pipe(
iter(argv),
filter(([key, _value]) => key.startsWith('link-')),
map(([key, value]) => [key.substr(5), value]),
obj,
);
const schemaPath = argv.d;
const outDir = argv.o;
const metadata = argv.m;
const schemaOut = argv.x !== '-' ? argv.x : null;
const includeReadme = !argv.n;
const i18n = argv.i;
const language = argv.l;
const exampleFormat = argv.f;
const includeProperties = argv.p;
const header = argv.h;
const skipProperties = argv.s;
const schemaExtension = argv.e;
// list all schema files in the specified directory
const schemaFiles = await readdirpPromise(
schemaPath,
{ root: schemaPath, fileFilter: (f) => f.path.endsWith(schemaExtension) },
);
console.log(`loading ${schemaFiles.length} schemas`);
/**
* @type {SchemaList[]}
* */
const schemas = schemaFiles.map((schema) => ({
fileName: schema.basename,
fullPath: schema.fullPath,
}));
jsonschema2md(schemas, {
schemaPath,
outDir,
metadata,
schemaOut,
includeReadme,
links,
i18n,
language,
exampleFormat,
includeProperties,
header,
skipProperties,
});
return 1;
}