Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import build from './markdownBuilder.js';
import { writereadme, writemarkdown } from './writeMarkdown.js';
import readme from './readmeBuilder.js';
import loader from './schemaProxy.js';
import s from './symbols.js';
import writeSchema from './writeSchema.js';

const __filename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -74,6 +75,8 @@ const { debug, info, error } = console;
* in markdown.
* @param {string[]} [options.skipProperties=[]] - (optional) Name of a default property to
* skip in markdown.
* @param {boolean} [options.singleFile=false] - (optional) Generate a single Markdown file
* per schema, inlining all property documentation instead of creating separate files.
* @returns {GeneratedOutput} List of raw markdown that were generated from input schema.
*/
export function jsonschema2md(schema, options) {
Expand All @@ -90,6 +93,7 @@ export function jsonschema2md(schema, options) {
includeProperties,
header,
skipProperties,
singleFile,
} = options;
if (!schema || typeof schema !== 'object') {
throw Error('Input is not valid. Provide JSON schema either as Object or Array.');
Expand Down Expand Up @@ -123,7 +127,7 @@ export function jsonschema2md(schema, options) {
const schemaLoader = loader();

// collect data about the schemas and turn everything into a big object
const schemas = pipe(
let schemas = pipe(
normalized,
// Checking if data contains the file path or its contents (JSON schema)
map(({ fileName, fullPath, content }) => {
Expand All @@ -135,6 +139,10 @@ export function jsonschema2md(schema, options) {
traverse,
);

if (singleFile) {
schemas = schemas.filter((item) => !item[s.parent]);
}

/**
* @type {GeneratedOutput}
*/
Expand Down Expand Up @@ -175,6 +183,7 @@ export function jsonschema2md(schema, options) {
includeProperties,
exampleFormat,
skipProperties,
singleFile,
rewritelinks: (origin) => {
const mddir = out;
if (!mddir) {
Expand Down Expand Up @@ -283,7 +292,14 @@ export async function main(args) {
.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', []);
.default('s', [])

.option('S', {
alias: 'single-file',
type: 'boolean',
describe: 'generate a single Markdown file per schema, inlining all property documentation',
default: false,
});

const links = pipe(
iter(argv),
Expand All @@ -303,6 +319,7 @@ export async function main(args) {
const includeProperties = argv.p;
const header = argv.h;
const skipProperties = argv.s;
const singleFile = argv.S;

const schemaExtension = argv.e;

Expand Down Expand Up @@ -335,6 +352,7 @@ export async function main(args) {
includeProperties,
header,
skipProperties,
singleFile,
});

return 1;
Expand Down
84 changes: 54 additions & 30 deletions lib/markdownBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,20 @@ export default function build({
includeProperties = [],
rewritelinks = (x) => x,
exampleFormat = 'json',
skipProperties = [],
skipProperties: rawSkipProperties = [],
singleFile = false,
} = {}) {
const skipProperties = singleFile
? [...new Set([...rawSkipProperties, 'definedinfact'])]
: rawSkipProperties;

function schemaLink(url, title, children) {
if (singleFile) {
return children;
}
return link(url, title, children);
}

const formats = {
'date-time': {
label: i18n`date time`,
Expand Down Expand Up @@ -322,17 +334,22 @@ export default function build({
* @param {*} param0
*/
function makepropheader(required = [], ispattern = false, slugger) {
return ([name, definition]) => tableRow([
tableCell(ispattern ? inlineCode(name) : link(`#${slugger.slug(name)}`, '', text(name))), // Property
tableCell(type(definition)),
tableCell(text(required.indexOf(name) > -1 ? i18n`Required` : i18n`Optional`)),
tableCell(nullable(definition)),
tableCell(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`),
)),
]);
return ([name, definition]) => {
const cells = [
tableCell(ispattern ? inlineCode(name) : link(`#${slugger.slug(name)}`, '', text(name))), // Property
tableCell(type(definition)),
tableCell(text(required.indexOf(name) > -1 ? i18n`Required` : i18n`Optional`)),
tableCell(nullable(definition)),
];
if (!singleFile) {
cells.push(tableCell(schemaLink(
`${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`),
)));
}
return tableRow(cells);
};
}

/**
Expand All @@ -353,27 +370,33 @@ export default function build({
const additionalproprows = (() => {
if (additionalProps) {
const any = additionalProps === true;
return [tableRow([
const cells = [
tableCell(text(i18n`Additional Properties`)),
tableCell(any ? text('Any') : type(additionalProps)),
tableCell(text(i18n`Optional`)),
tableCell(any ? text('can be null') : nullable(additionalProps)),
tableCell(any ? text('') : link(`${additionalProps[s.slug]}.md`, `${additionalProps[s.id]}#${additionalProps[s.pointer]}`, text(additionalProps[s.titles][0] || i18n`Untitled schema`))),
])];
];
if (!singleFile) {
cells.push(tableCell(any ? text('') : schemaLink(`${additionalProps[s.slug]}.md`, `${additionalProps[s.id]}#${additionalProps[s.pointer]}`, text(additionalProps[s.titles][0] || i18n`Untitled schema`))));
}
return [tableRow(cells)];
}
return [];
})();

// const proprows = flist(map(iter(props || {}), makepropheader(required)));

const headerCells = [
tableCell(text(i18n`Property`)),
tableCell(text(i18n`Type`)),
tableCell(text(i18n`Required`)),
tableCell(text(i18n`Nullable`)),
];
if (!singleFile) {
headerCells.push(tableCell(text(i18n`Defined by`)));
}
return table('left', [
tableRow([
tableCell(text(i18n`Property`)),
tableCell(text(i18n`Type`)),
tableCell(text(i18n`Required`)),
tableCell(text(i18n`Nullable`)),
tableCell(text(i18n`Defined by`)),
]),
tableRow(headerCells),
...proprows,
...patternproprows,
...additionalproprows,
Expand All @@ -388,7 +411,7 @@ export default function build({
paragraph([text(i18n`Type: `), text(i18n`an array where each item follows the corresponding schema in the following list:`)]),
list(
'ordered',
[...items.map((schema) => listItem(paragraph(link(
[...items.map((schema) => listItem(paragraph(schemaLink(
`${schema[s.slug]}.md`,
i18n`check type definition`,
text(gentitle(schema[s.titles], schema[keyword`type`])),
Expand All @@ -398,7 +421,7 @@ export default function build({
return [listItem(paragraph(text(i18n`and all following items may follow any schema`)))];
} else if (typeof additional === 'object') {
return [listItem(paragraph([text(i18n`and all following items must follow the schema: `),
link(
schemaLink(
`${additional[s.slug]}.md`,
i18n`check type definition`,
text(gentitle(additional[s.titles], additional[keyword`type`])),
Expand Down Expand Up @@ -448,8 +471,9 @@ export default function build({
const typelink = (() => {
if (definition[keyword`title`] && typeof definition[keyword`title`] === 'string') {
// if the type has a title, always create a link to the schema
return [text(' ('), link(`${definition[s.slug]}.md`, '', text(definition[keyword`title`])), text(')')];
return [text(' ('), schemaLink(`${definition[s.slug]}.md`, '', text(definition[keyword`title`])), text(')')];
} else if (!singletype || firsttype === keyword`object` || merged) {
if (singleFile) return [];
return [text(' ('), link(`${definition[s.slug]}.md`, '', text(i18n`Details`)), text(')')];
}
return [];
Expand All @@ -473,7 +497,7 @@ export default function build({
function makedefinedinfact(definition) {
return listItem(paragraph([
text(i18n`defined in: `),
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`)),
schemaLink(`${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`)),
]));
}

Expand Down Expand Up @@ -544,7 +568,7 @@ export default function build({
];
} else if (depth > 0) {
return [
link(`${schema[s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[s.titles], schema[keyword`type`]))),
schemaLink(`${schema[s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[s.titles], schema[keyword`type`]))),
];
} else {
return [];
Expand Down Expand Up @@ -645,7 +669,7 @@ export default function build({
constraints.push(paragraph([
strong(text(i18n`schema`)), text(': '),
text(i18n`the contents of this string should follow this schema: `),
link(`${schema[keyword`contentSchema`][s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[keyword`contentSchema`][s.titles], schema[keyword`contentSchema`][keyword`type`])))]));
schemaLink(`${schema[keyword`contentSchema`][s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[keyword`contentSchema`][s.titles], schema[keyword`contentSchema`][keyword`type`])))]));
}

// https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.4
Expand All @@ -664,12 +688,12 @@ export default function build({
if (schema[keyword`minContains`] !== undefined && schema[keyword`contains`]) {
// console.log('minContains!', schema[s.filename], schema[s.pointer]);
constraints.push(paragraph([strong(text(i18n`minimum number of contained items`)), text(': '), text(`${i18n`this array may not contain fewer than ${String(schema[keyword`minContains`])} items that validate against the schema:`} `),
link(`${schema[keyword`contains`][s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[keyword`contains`][s.titles], schema[keyword`contains`][keyword`type`])))]));
schemaLink(`${schema[keyword`contains`][s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[keyword`contains`][s.titles], schema[keyword`contains`][keyword`type`])))]));
}
if (schema[keyword`maxContains`] !== undefined && schema[keyword`contains`]) {
// console.log('maxContains!', schema[s.filename], schema[s.pointer]);
constraints.push(paragraph([strong(text(i18n`maximum number of contained items`)), text(': '), text(`${i18n`this array may not contain more than ${String(schema[keyword`maxContains`])} items that validate against the schema:`} `),
link(`${schema[keyword`contains`][s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[keyword`contains`][s.titles], schema[keyword`contains`][keyword`type`])))]));
schemaLink(`${schema[keyword`contains`][s.slug]}.md`, i18n`check type definition`, text(gentitle(schema[keyword`contains`][s.titles], schema[keyword`contains`][keyword`type`])))]));
}

// https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.5
Expand Down
13 changes: 13 additions & 0 deletions test/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ The schemas linked above follow the JSON Schema Spec version: \`http://json-sche
.contains('## examples');
});

it('Public API with singleFile generates one markdown per top-level schema', async () => {
const result = jsonschema2md(example, {
includeReadme: false,
singleFile: true,
});
assert.strictEqual(result.markdown.length, 1);
assertMarkdown(result.markdown[0].markdownAst)
.contains('## foo')
.contains('## bar')
.doesNotContain('defined in:')
.doesNotContain('Defined by');
});

it('Public API with invalid schema', async () => {
try {
jsonschema2md('test', {
Expand Down
23 changes: 23 additions & 0 deletions test/markdownBuilder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,29 @@ describe('Testing Markdown Builder: Skip properties', () => {
});
});

describe('Testing Markdown Builder: singleFile', () => {
it('Single-file mode omits Defined by column and defined in fact', async () => {
const schemas = await traverseSchemas('type');
const builder = build({ header: false, singleFile: true });
const results = builder(schemas);

assertMarkdown(results.type)
.doesNotContain('Defined by')
.doesNotContain('defined in:')
.doesNotContain('.md');
});

it('Single-file mode strips file links from type references', async () => {
const schemas = await traverseSchemas('type');
const builder = build({ header: false, singleFile: true });
const results = builder(schemas);

assertMarkdown(results.button)
.contains('Properties')
.doesNotContain('button-properties-properties.md');
});
});

describe('Testing Markdown Builder: boolean defaults', () => {
let results;

Expand Down