From e243b0dcd9821432210b3426bbf165dca7aa1204 Mon Sep 17 00:00:00 2001 From: James Brumond Date: Sat, 13 May 2023 19:26:25 -0700 Subject: [PATCH] copy over old code --- package-lock.json | 38 ++ package.json | 3 + src/detect-version.ts | 79 ++++ src/index.ts | 952 +++++++++++++++++++++------------------- src/json-pointer.ts | 110 +++++ src/markdown-builder.ts | 317 +++++++++++++ src/section-ids.ts | 25 ++ 7 files changed, 1061 insertions(+), 463 deletions(-) create mode 100644 src/detect-version.ts create mode 100644 src/json-pointer.ts create mode 100644 src/markdown-builder.ts create mode 100644 src/section-ids.ts diff --git a/package-lock.json b/package-lock.json index 3d88fc1..6a36ffd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,17 @@ "version": "0.1.0", "dependencies": { "glob": "^10.2.2", + "json-schema": "^0.4.0", "luxon": "^3.3.0", "mustache": "^4.2.0", + "word-wrap": "^1.2.3", "yaml": "^2.2.2" }, "bin": { "docs2website": "bin/docs2website" }, "devDependencies": { + "@types/json-schema": "^7.0.11", "@types/luxon": "^3.1.0", "@types/mustache": "^4.2.2", "@types/node": "^18.16.3", @@ -48,6 +51,12 @@ "node": ">=14" } }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, "node_modules/@types/luxon": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz", @@ -206,6 +215,11 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, "node_modules/lru-cache": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz", @@ -420,6 +434,14 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -533,6 +555,12 @@ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "optional": true }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, "@types/luxon": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz", @@ -647,6 +675,11 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, "lru-cache": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz", @@ -784,6 +817,11 @@ "isexe": "^2.0.0" } }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, "wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/package.json b/package.json index 584b93d..7f2b9d3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "main": "./build/index.js", "devDependencies": { + "@types/json-schema": "^7.0.11", "@types/luxon": "^3.1.0", "@types/mustache": "^4.2.2", "@types/node": "^18.16.3", @@ -20,8 +21,10 @@ }, "dependencies": { "glob": "^10.2.2", + "json-schema": "^0.4.0", "luxon": "^3.3.0", "mustache": "^4.2.0", + "word-wrap": "^1.2.3", "yaml": "^2.2.2" } } diff --git a/src/detect-version.ts b/src/detect-version.ts new file mode 100644 index 0000000..d639a26 --- /dev/null +++ b/src/detect-version.ts @@ -0,0 +1,79 @@ + +import { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'; + +const v4_schemas = new Set([ + 'http://json-schema.org/schema#', + 'https://json-schema.org/schema#', + 'http://json-schema.org/schema', + 'https://json-schema.org/schema', + 'http://json-schema.org/hyper-schema#', + 'https://json-schema.org/hyper-schema#', + 'http://json-schema.org/hyper-schema', + 'https://json-schema.org/hyper-schema', + 'http://json-schema.org/draft-04/schema#', + 'https://json-schema.org/draft-04/schema#', + 'http://json-schema.org/draft-04/schema', + 'https://json-schema.org/draft-04/schema', + 'http://json-schema.org/draft-04/hyper-schema#', + 'https://json-schema.org/draft-04/hyper-schema#', + 'http://json-schema.org/draft-04/hyper-schema', + 'https://json-schema.org/draft-04/hyper-schema', + 'http://json-schema.org/draft-03/schema#', + 'https://json-schema.org/draft-03/schema#', + 'http://json-schema.org/draft-03/schema', + 'https://json-schema.org/draft-03/schema', + 'http://json-schema.org/draft-03/hyper-schema#', + 'https://json-schema.org/draft-03/hyper-schema#', + 'http://json-schema.org/draft-03/hyper-schema', + 'https://json-schema.org/draft-03/hyper-schema', +]) + +export function is_json_schema_draft4(data: unknown) : data is JSONSchema4 { + return v4_schemas.has(data?.['$schema']); +} + +const v6_schemas = new Set([ + 'http://json-schema.org/schema#', + 'https://json-schema.org/schema#', + 'http://json-schema.org/schema', + 'https://json-schema.org/schema', + 'http://json-schema.org/hyper-schema#', + 'https://json-schema.org/hyper-schema#', + 'http://json-schema.org/hyper-schema', + 'https://json-schema.org/hyper-schema', + 'http://json-schema.org/draft-06/schema#', + 'https://json-schema.org/draft-06/schema#', + 'http://json-schema.org/draft-06/schema', + 'https://json-schema.org/draft-06/schema', + 'http://json-schema.org/draft-06/hyper-schema#', + 'https://json-schema.org/draft-06/hyper-schema#', + 'http://json-schema.org/draft-06/hyper-schema', + 'https://json-schema.org/draft-06/hyper-schema', +]); + +export function is_json_schema_draft6(data: unknown) : data is JSONSchema6 { + return v6_schemas.has(data?.['$schema']); +} + +const v7_schemas = new Set([ + 'http://json-schema.org/schema#', + 'https://json-schema.org/schema#', + 'http://json-schema.org/schema', + 'https://json-schema.org/schema', + 'http://json-schema.org/hyper-schema#', + 'https://json-schema.org/hyper-schema#', + 'http://json-schema.org/hyper-schema', + 'https://json-schema.org/hyper-schema', + 'http://json-schema.org/draft-07/schema#', + 'https://json-schema.org/draft-07/schema#', + 'http://json-schema.org/draft-07/schema', + 'https://json-schema.org/draft-07/schema', + 'http://json-schema.org/draft-07/hyper-schema#', + 'https://json-schema.org/draft-07/hyper-schema#', + 'http://json-schema.org/draft-07/hyper-schema', + 'https://json-schema.org/draft-07/hyper-schema', +]); + +export function is_json_schema_draft7(data: unknown) : data is JSONSchema7 { + return v7_schemas.has(data?.['$schema']); +} diff --git a/src/index.ts b/src/index.ts index 3e7f258..a315126 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,575 +1,601 @@ -// import * as wrap from 'word-wrap'; -// import { JSONSchema7 } from 'json-schema'; -// import { JSONSchema, JSONSchemaDefinition, SchemaDoc } from '../../documents'; -// import { MarkdownBuilder } from './builder'; -// import { JsonPointer } from '../../utils'; -// import { render_document_formats } from './formats-section'; +import * as wrap from 'word-wrap'; +import { MarkdownBuilder } from './markdown-builder'; +import { jsonptr, JsonPointer } from './json-pointer'; +import { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'; +import { is_json_schema_draft4, is_json_schema_draft6, is_json_schema_draft7 } from './detect-version'; -// const indent_with = ' '; +type JSONSchema = JSONSchema4 | JSONSchema6 | JSONSchema7; -export function build_markdown_from_json_schema(/* json_schema: SchemaDoc */) /* : string */ { } -// const schema = json_schema.data; -// const doc = new MarkdownBuilder({ -// numbered_headings: false, -// }); +const indent_with = ' '; -// doc.h1(schema.title, { id: '/title' }); - -// if (schema.description) { -// doc.p(schema.description); -// } - -// render_document_formats(doc, json_schema.json_media_type, { -// json: json_schema.json_media_type, -// yaml: json_schema.yaml_media_type, -// }); - -// doc.h2('Schema', { id: '/' }); +export function build_markdown_from_json_schema(raw: unknown) : string { + if (is_json_schema_draft7(raw)) { + return build_markdown_from_json_schema_v7(raw); + } -// const close_block = doc.code_block({ lang: 'clike' }); -// doc.raw(render_type(schema) + '\n\n'); -// close_block(); - -// if (schema.definitions) { -// const ptr = new JsonPointer([ 'definitions' ]); - -// doc.h2('Definitions', { id: ptr.as_md_str() }); - -// for (const [ name, type ] of Object.entries(schema.definitions) as [ string, JSONSchemaDefinition ][]) { -// if (name === '//') { -// continue; -// } - -// doc.h3(name, { id: ptr.step_down(name).as_md_str() }); - -// const close_block = doc.code_block({ lang: 'clike' }); -// doc.raw(render_comment_if_needed(type) + 'def ' + name + ': ' + render_type(type) + '\n'); -// close_block(); -// } -// } - -// return doc.as_str(); -// } - -// function render_comment_if_needed(obj: JSONSchema | boolean, indent_depth = 0) { -// if (typeof obj === 'boolean') { -// return ''; -// } - -// const indent = `${indent_with.repeat(indent_depth)}// `; - -// const chunks: string[] = [ ]; - -// if (obj.title) { -// chunks.push(indent + obj.title + '\n'); -// } - -// if (obj.description) { -// chunks.push(wrap(obj.description, { indent, width: 100, }) + '\n'); -// } - -// if (Array.isArray(obj.examples)) { -// chunks.push( -// indent + 'Examples:\n' + obj.examples.map((example) => { -// return indent + '- ' + JSON.stringify(example) + '\n' -// }).join('') -// ); -// } - -// if ((obj as JSONSchema7).$comment) { -// chunks.push( -// indent + '--- Comment ---\n' + -// wrap((obj as JSONSchema7).$comment, { indent, width: 100, }) + '\n' -// ); -// } - -// return chunks.join(indent + '\n'); -// } - -// function render_schema(doc: SchemaDoc, indent_depth = 0, inline = true) { -// const schema = doc.data; -// const chunks: string[] = [ ]; -// const indent = indent_with.repeat(indent_depth); - -// chunks.push(render_comment_if_needed(schema, indent_depth)); -// chunks.push((inline ? '' : indent) + 'schema ' + (schema.$id ? str(schema.$id) + ' {\n' : '{\n')); -// chunks.push(render_type(schema, indent_depth + 1, false)); - -// if (schema.definitions) { -// chunks.push('\n'); -// chunks.push(render_definitions(schema, indent_depth + 1)); -// } - -// chunks.push('}'); - -// return chunks.join(''); -// } - -// function render_type(obj: JSONSchema | boolean, indent_depth = 0, inline = true) { -// if (obj === true) { -// return render_simple('any', indent_depth, inline); -// } + if (is_json_schema_draft6(raw)) { + return build_markdown_from_json_schema_v6(raw); + } -// if (obj === false) { -// return render_simple('never', indent_depth, inline); -// } + if (is_json_schema_draft4(raw)) { + return build_markdown_from_json_schema_v4(raw); + } -// const chunks: string[] = [ ]; -// const core_type = render_core_type_details(obj, indent_depth, inline); + throw new Error('JSON schema document not recognized (does it have a valid $schema field?)'); +} -// if (core_type) { -// chunks.push(core_type); -// } +function build_markdown_from_json_schema_v7(schema: JSONSchema7) : string { + const doc = new MarkdownBuilder({ + numbered_headings: false, + }); -// const indent = indent_with.repeat(indent_depth); + doc.h1(schema.title, { id: jsonptr('title') }); -// if (obj.oneOf) { -// chunks.push(render_additional(obj, render_one_of)); -// } + if (schema.description) { + doc.p(schema.description); + } -// if (obj.anyOf) { -// chunks.push(render_additional(obj, render_any_of)); -// } + doc.h2('Schema', { id: jsonptr() }); + + const close_block = doc.code_block({ lang: 'clike' }); + doc.raw(render_type(schema) + '\n\n'); + close_block(); -// if (obj.allOf) { -// chunks.push(render_additional(obj, render_all_of)); -// } + if (schema.definitions) { + const ptr = new JsonPointer([ 'definitions' ]); -// if (obj.not) { -// chunks.push(render_additional(obj, render_not)); -// } + doc.h2('Definitions', { id: ptr.as_md_str() }); -// // todo: if -// // todo: then -// // todo: else + for (const [ name, type ] of Object.entries(schema.definitions) as [ string, JSONSchema['definitions'][string] ][]) { + if (name === '//') { + continue; + } -// if (! chunks.length) { -// return render_any(obj, indent_depth, inline); -// } + doc.h3(name, { id: ptr.step_down(name).as_md_str() }); -// if (inline) { -// if (! chunks[chunks.length - 1].endsWith(';')) { -// chunks.push(';'); -// } -// } + const close_block = doc.code_block({ lang: 'clike' }); + doc.raw(render_comment_if_needed(type) + 'def ' + name + ': ' + render_type(type) + '\n'); + close_block(); + } + } + + return doc.as_str(); +} -// else if (! chunks[chunks.length - 1].endsWith(';\n')) { -// if (chunks[chunks.length - 1].endsWith('\n')) { -// chunks[chunks.length - 1] = chunks[chunks.length - 1].slice(0, -1) + ';\n'; -// } +function build_markdown_from_json_schema_v6(schema: JSONSchema6) : string { + return 'JSON schema draft 6 not currently supported'; +} + +function build_markdown_from_json_schema_v4(schema: JSONSchema4) : string { + return 'JSON schema draft 4 not currently supported'; +} + + + + + + + +// ===== Copied over from old code, needs review ===== + +function render_comment_if_needed(obj: JSONSchema | boolean, indent_depth = 0) { + if (typeof obj === 'boolean') { + return ''; + } + + const indent = `${indent_with.repeat(indent_depth)}// `; + + const chunks: string[] = [ ]; + + if (obj.title) { + chunks.push(indent + obj.title + '\n'); + } + + if (obj.description) { + chunks.push(wrap(obj.description, { indent, width: 100, }) + '\n'); + } + + if (Array.isArray(obj.examples)) { + chunks.push( + indent + 'Examples:\n' + obj.examples.map((example) => { + return indent + '- ' + JSON.stringify(example) + '\n' + }).join('') + ); + } + + if ((obj as JSONSchema7).$comment) { + chunks.push( + indent + '--- Comment ---\n' + + wrap((obj as JSONSchema7).$comment, { indent, width: 100, }) + '\n' + ); + } + + return chunks.join(indent + '\n'); +} + +function render_schema(schema: JSONSchema, indent_depth = 0, inline = true) { + const chunks: string[] = [ ]; + const indent = indent_with.repeat(indent_depth); + + chunks.push(render_comment_if_needed(schema, indent_depth)); + chunks.push((inline ? '' : indent) + 'schema ' + (schema.$id ? str(schema.$id) + ' {\n' : '{\n')); + chunks.push(render_type(schema, indent_depth + 1, false)); + + if (schema.definitions) { + chunks.push('\n'); + chunks.push(render_definitions(schema, indent_depth + 1)); + } + + chunks.push('}'); + + return chunks.join(''); +} + +function render_type(obj: JSONSchema | boolean, indent_depth = 0, inline = true) { + if (obj === true) { + return render_simple('any', indent_depth, inline); + } + + if (obj === false) { + return render_simple('never', indent_depth, inline); + } + + const chunks: string[] = [ ]; + const core_type = render_core_type_details(obj, indent_depth, inline); + + if (core_type) { + chunks.push(core_type); + } + + const indent = indent_with.repeat(indent_depth); + + if (obj.oneOf) { + chunks.push(render_additional(obj, render_one_of)); + } + + if (obj.anyOf) { + chunks.push(render_additional(obj, render_any_of)); + } + + if (obj.allOf) { + chunks.push(render_additional(obj, render_all_of)); + } + + if (obj.not) { + chunks.push(render_additional(obj, render_not)); + } + + // todo: if + // todo: then + // todo: else + + if (! chunks.length) { + return render_any(obj, indent_depth, inline); + } + + if (inline) { + if (! chunks[chunks.length - 1].endsWith(';')) { + chunks.push(';'); + } + } + + else if (! chunks[chunks.length - 1].endsWith(';\n')) { + if (chunks[chunks.length - 1].endsWith('\n')) { + chunks[chunks.length - 1] = chunks[chunks.length - 1].slice(0, -1) + ';\n'; + } -// else { -// chunks.push( -// chunks[chunks.length - 1].endsWith(';') ? '\n' : ';\n' -// ); -// } -// } + else { + chunks.push( + chunks[chunks.length - 1].endsWith(';') ? '\n' : ';\n' + ); + } + } -// return chunks.join(''); + return chunks.join(''); -// function render_additional(obj: JSONSchema, render: (obj: JSONSchema, indent_depth: number, inline: boolean) => string) { -// if (chunks.length) { -// const newline = inline ? '\n' : ''; -// return newline + indent + '+ ' + render(obj, indent_depth, true); -// } + function render_additional(obj: JSONSchema, render: (obj: JSONSchema, indent_depth: number, inline: boolean) => string) { + if (chunks.length) { + const newline = inline ? '\n' : ''; + return newline + indent + '+ ' + render(obj, indent_depth, true); + } -// return render(obj, indent_depth, inline); -// } -// } + return render(obj, indent_depth, inline); + } +} -// function render_core_type_details(obj: JSONSchema, indent_depth = 0, inline = true) { -// if (obj.$ref) { -// return render_ref(obj, indent_depth, inline); -// } +function render_core_type_details(obj: JSONSchema, indent_depth = 0, inline = true) { + if (obj.$ref) { + return render_ref(obj, indent_depth, inline); + } -// if (Array.isArray(obj.type)) { -// const is_obj = obj.type.includes('object'); -// const is_arr = obj.type.includes('array'); + if (Array.isArray(obj.type)) { + const is_obj = obj.type.includes('object'); + const is_arr = obj.type.includes('array'); -// if (is_obj && is_arr) { -// // todo: render schemas that can be both objects and arrays somehow... -// } + if (is_obj && is_arr) { + // todo: render schemas that can be both objects and arrays somehow... + } -// else if (is_obj) { -// return render_object(obj, indent_depth, inline) -// } + else if (is_obj) { + return render_object(obj, indent_depth, inline) + } -// else if (is_arr) { -// // todo: render multi-type arrays -// } + else if (is_arr) { + // todo: render multi-type arrays + } -// else { -// return render_basic(obj, indent_depth, inline); -// } -// } + else { + return render_basic(obj, indent_depth, inline); + } + } -// const type = obj.type || infer_type(obj); + const type = obj.type || infer_type(obj); -// switch (type) { -// case 'any': return render_any(obj, indent_depth, inline); -// case 'array': return render_array(obj, indent_depth, inline); -// case 'boolean': return render_basic(obj, indent_depth, inline); -// case 'integer': return render_basic(obj, indent_depth, inline); -// case 'null': return render_simple('null', indent_depth, inline); -// case 'number': return render_basic(obj, indent_depth, inline); -// case 'object': return render_object(obj, indent_depth, inline); -// case 'string': return render_basic(obj, indent_depth, inline); -// } -// } + switch (type) { + case 'any': return render_any(obj, indent_depth, inline); + case 'array': return render_array(obj, indent_depth, inline); + case 'boolean': return render_basic(obj, indent_depth, inline); + case 'integer': return render_basic(obj, indent_depth, inline); + case 'null': return render_simple('null', indent_depth, inline); + case 'number': return render_basic(obj, indent_depth, inline); + case 'object': return render_object(obj, indent_depth, inline); + case 'string': return render_basic(obj, indent_depth, inline); + } +} -// // ===== Type-Specific Renderers ===== +// ===== Type-Specific Renderers ===== -// function render_simple(text: string, indent_depth = 0, inline = true) { -// const indent = indent_with.repeat(indent_depth); -// return inline ? `${text};` : (indent + `${text};\n`); -// } +function render_simple(text: string, indent_depth = 0, inline = true) { + const indent = indent_with.repeat(indent_depth); + return inline ? `${text};` : (indent + `${text};\n`); +} -// function render_basic_type_and_attributes(obj: JSONSchema, type_str?: string, indent_depth = 0, inline = true) { -// const indent = indent_with.repeat(indent_depth); -// const basic_type = type_str || ( -// Array.isArray(obj.type) ? obj.type.join(' | ') : (obj.type || 'any') -// ); -// const chunks: string[] = [ (inline ? '' : indent) + basic_type ]; -// const attrs: string[] = [ ]; +function render_basic_type_and_attributes(obj: JSONSchema, type_str?: string, indent_depth = 0, inline = true) { + const indent = indent_with.repeat(indent_depth); + const basic_type = type_str || ( + Array.isArray(obj.type) ? obj.type.join(' | ') : (obj.type || 'any') + ); + const chunks: string[] = [ (inline ? '' : indent) + basic_type ]; + const attrs: string[] = [ ]; -// if ('const' in obj) { -// attrs.push(`(const ${JSON.stringify(obj.const)})`); -// } + if ('const' in obj) { + attrs.push(`(const ${JSON.stringify(obj.const)})`); + } -// if ('enum' in obj) { -// const values = obj.enum.map((value) => JSON.stringify(value)); -// const value_list = render_inline_list(values, 1); -// attrs.push(`(enum${value_list})`); -// } + if ('enum' in obj) { + const values = obj.enum.map((value) => JSON.stringify(value)); + const value_list = render_inline_list(values, 1); + attrs.push(`(enum${value_list})`); + } -// if ('default' in obj) { -// attrs.push(`(default ${JSON.stringify(obj.default)})`); -// } + if ('default' in obj) { + attrs.push(`(default ${JSON.stringify(obj.default)})`); + } -// if ('minimum' in obj) { -// attrs.push(`(minimum ${obj.minimum})`); -// } + if ('minimum' in obj) { + attrs.push(`(minimum ${obj.minimum})`); + } -// if ('maximum' in obj) { -// attrs.push(`(maximum ${obj.maximum})`); -// } + if ('maximum' in obj) { + attrs.push(`(maximum ${obj.maximum})`); + } -// if ('exclusiveMinimum' in obj) { -// attrs.push(`(exclusive-minimum ${obj.exclusiveMinimum})`); -// } + if ('exclusiveMinimum' in obj) { + attrs.push(`(exclusive-minimum ${obj.exclusiveMinimum})`); + } -// if ('exclusiveMaximum' in obj) { -// attrs.push(`(exclusive-maximum ${obj.exclusiveMaximum})`); -// } + if ('exclusiveMaximum' in obj) { + attrs.push(`(exclusive-maximum ${obj.exclusiveMaximum})`); + } -// if ('multipleOf' in obj) { -// attrs.push(`(multiple-of ${obj.multipleOf})`); -// } + if ('multipleOf' in obj) { + attrs.push(`(multiple-of ${obj.multipleOf})`); + } -// if ('minItems' in obj) { -// attrs.push(`(min-items ${obj.minItems})`); -// } + if ('minItems' in obj) { + attrs.push(`(min-items ${obj.minItems})`); + } -// if ('maxItems' in obj) { -// attrs.push(`(max-items ${obj.maxItems})`); -// } + if ('maxItems' in obj) { + attrs.push(`(max-items ${obj.maxItems})`); + } -// if ('uniqueItems' in obj) { -// attrs.push('(unique-items)'); -// } + if ('uniqueItems' in obj) { + attrs.push('(unique-items)'); + } -// if ('format' in obj) { -// attrs.push(`(format ${obj.format})`); -// } + if ('format' in obj) { + attrs.push(`(format ${obj.format})`); + } -// if ('pattern' in obj) { -// attrs.push(`(pattern ${regex(obj.pattern)})`); -// } + if ('pattern' in obj) { + attrs.push(`(pattern ${regex(obj.pattern)})`); + } -// if ('minLength' in obj) { -// attrs.push(`(min-length ${obj.minLength})`); -// } + if ('minLength' in obj) { + attrs.push(`(min-length ${obj.minLength})`); + } -// if ('maxLength' in obj) { -// attrs.push(`(max-length ${obj.maxLength})`); -// } + if ('maxLength' in obj) { + attrs.push(`(max-length ${obj.maxLength})`); + } -// if ('contentMediaType' in obj) { -// attrs.push(`(content-media-type ${str(obj.contentMediaType)})`); -// } + if ('contentMediaType' in obj) { + attrs.push(`(content-media-type ${str(obj.contentMediaType)})`); + } -// if ('contentEncoding' in obj) { -// attrs.push(`(content-encoding ${str(obj.contentEncoding)})`); -// } + if ('contentEncoding' in obj) { + attrs.push(`(content-encoding ${str(obj.contentEncoding)})`); + } -// chunks.push(render_inline_list(attrs, indent_depth + 1)); + chunks.push(render_inline_list(attrs, indent_depth + 1)); -// return chunks; -// } + return chunks; +} -// function render_ref(obj: JSONSchema, indent_depth = 0, inline = true) { -// const chunks = render_basic_type_and_attributes(obj, `<${str(obj.$ref)}>`, indent_depth, inline); -// return chunks.join('') + (inline ? '' : '\n'); -// } +function render_ref(obj: JSONSchema, indent_depth = 0, inline = true) { + const chunks = render_basic_type_and_attributes(obj, `<${str(obj.$ref)}>`, indent_depth, inline); + return chunks.join('') + (inline ? '' : '\n'); +} -// function render_any(obj: JSONSchema, indent_depth = 0, inline = true) { -// const chunks = render_basic_type_and_attributes(obj, 'any', indent_depth, inline); -// return chunks.join('') + (inline ? '' : '\n'); -// } +function render_any(obj: JSONSchema, indent_depth = 0, inline = true) { + const chunks = render_basic_type_and_attributes(obj, 'any', indent_depth, inline); + return chunks.join('') + (inline ? '' : '\n'); +} -// function render_basic(obj: JSONSchema, indent_depth = 0, inline = true) { -// const chunks = render_basic_type_and_attributes(obj, null, indent_depth, inline); -// return chunks.join('') + (inline ? '' : '\n'); -// } +function render_basic(obj: JSONSchema, indent_depth = 0, inline = true) { + const chunks = render_basic_type_and_attributes(obj, null, indent_depth, inline); + return chunks.join('') + (inline ? '' : '\n'); +} -// function render_array(obj: JSONSchema, indent_depth = 0, inline = true) { -// const indent = indent_with.repeat(indent_depth); -// const chunks = render_basic_type_and_attributes(obj, null, indent_depth, inline); +function render_array(obj: JSONSchema, indent_depth = 0, inline = true) { + const indent = indent_with.repeat(indent_depth); + const chunks = render_basic_type_and_attributes(obj, null, indent_depth, inline); -// if (obj.items) { -// if (Array.isArray(obj.items)) { -// chunks.push(' [\n'); -// chunks.push(render_array_tuple(obj, indent_depth + 1)); -// chunks.push(indent + ']'); -// } + if (obj.items) { + if (Array.isArray(obj.items)) { + chunks.push(' [\n'); + chunks.push(render_array_tuple(obj, indent_depth + 1)); + chunks.push(indent + ']'); + } -// else { -// chunks.push(' {\n'); -// chunks.push(render_comment_if_needed(obj.items, indent_depth + 1)); -// chunks.push(render_type(obj.items, indent_depth + 1, false)); -// chunks.push(indent + '}'); -// } -// } + else { + chunks.push(' {\n'); + chunks.push(render_comment_if_needed(obj.items, indent_depth + 1)); + chunks.push(render_type(obj.items, indent_depth + 1, false)); + chunks.push(indent + '}'); + } + } -// return chunks.join('') + (inline ? '' : '\n'); -// } + return chunks.join('') + (inline ? '' : '\n'); +} -// function render_array_tuple(obj: JSONSchema, indent_depth = 0) { -// const chunks: string[] = [ ]; -// const indent = indent_with.repeat(indent_depth); +function render_array_tuple(obj: JSONSchema, indent_depth = 0) { + const chunks: string[] = [ ]; + const indent = indent_with.repeat(indent_depth); -// for (const type of (obj.items as JSONSchemaDefinition[])) { -// if (chunks.length) { -// chunks.push('\n'); -// } + for (const type of (obj.items as JSONSchema['definitions'][])) { + if (chunks.length) { + chunks.push('\n'); + } -// chunks.push(render_comment_if_needed(type, indent_depth)); -// chunks.push(indent + render_type(type, indent_depth) + '\n'); -// } + chunks.push(render_comment_if_needed(type, indent_depth)); + chunks.push(indent + render_type(type, indent_depth) + '\n'); + } -// if (obj.additionalItems) { -// if (chunks.length) { -// chunks.push('\n'); -// } + if (obj.additionalItems) { + if (chunks.length) { + chunks.push('\n'); + } -// chunks.push(render_comment_if_needed(obj.additionalItems, indent_depth)); -// chunks.push(indent + '... ' + render_type(obj.additionalItems, indent_depth) + '\n'); -// } + chunks.push(render_comment_if_needed(obj.additionalItems, indent_depth)); + chunks.push(indent + '... ' + render_type(obj.additionalItems, indent_depth) + '\n'); + } -// return chunks.join(''); -// } + return chunks.join(''); +} -// function render_object(obj: JSONSchema, indent_depth = 0, inline = true) { -// const indent = indent_with.repeat(indent_depth); -// const chunks = render_basic_type_and_attributes(obj, null, indent_depth, inline); +function render_object(obj: JSONSchema, indent_depth = 0, inline = true) { + const indent = indent_with.repeat(indent_depth); + const chunks = render_basic_type_and_attributes(obj, null, indent_depth, inline); -// if (obj.properties || obj.additionalProperties || obj.patternProperties) { -// chunks.push(' {\n'); -// chunks.push(render_object_properties(obj, indent_depth + 1)); -// chunks.push(indent + '}'); -// } + if (obj.properties || obj.additionalProperties || obj.patternProperties) { + chunks.push(' {\n'); + chunks.push(render_object_properties(obj, indent_depth + 1)); + chunks.push(indent + '}'); + } -// else if (Array.isArray(obj.required)) { -// const values = obj.required.map((value) => JSON.stringify(value)); -// const value_list = render_inline_list(values, 1); -// chunks.push(` (required${value_list})`); -// } + else if (Array.isArray(obj.required)) { + const values = obj.required.map((value) => JSON.stringify(value)); + const value_list = render_inline_list(values, 1); + chunks.push(` (required${value_list})`); + } -// return chunks.join('') + (inline ? '' : '\n'); -// } + return chunks.join('') + (inline ? '' : '\n'); +} -// function render_object_properties(obj: JSONSchema, indent_depth = 0) { -// const chunks: string[] = [ ]; -// const indent = indent_with.repeat(indent_depth); -// const required = new Set(Array.isArray(obj.required) ? obj.required : [ ]); +function render_object_properties(obj: JSONSchema, indent_depth = 0) { + const chunks: string[] = [ ]; + const indent = indent_with.repeat(indent_depth); + const required = new Set(Array.isArray(obj.required) ? obj.required : [ ]); -// if (obj.properties) { -// for (const [name, type] of Object.entries(obj.properties) as [string, JSONSchema['properties'][string]][]) { -// if (chunks.length) { -// chunks.push('\n'); -// } + if (obj.properties) { + for (const [name, type] of Object.entries(obj.properties) as [string, JSONSchema['properties'][string]][]) { + if (chunks.length) { + chunks.push('\n'); + } -// const optional_ind = required.has(name) ? '' : '?'; -// chunks.push(render_comment_if_needed(type, indent_depth)); -// chunks.push(indent + name + optional_ind + ': ' + render_type(type, indent_depth) + '\n'); -// } -// } + const optional_ind = required.has(name) ? '' : '?'; + chunks.push(render_comment_if_needed(type, indent_depth)); + chunks.push(indent + name + optional_ind + ': ' + render_type(type, indent_depth) + '\n'); + } + } -// if (obj.patternProperties) { -// for (const [name, type] of Object.entries(obj.patternProperties) as [string, JSONSchema['properties'][string]][]) { -// if (chunks.length) { -// chunks.push('\n'); -// } + if (obj.patternProperties) { + for (const [name, type] of Object.entries(obj.patternProperties) as [string, JSONSchema['properties'][string]][]) { + if (chunks.length) { + chunks.push('\n'); + } -// chunks.push(render_comment_if_needed(type, indent_depth)); -// chunks.push(indent + `['${name}']: ` + render_type(type, indent_depth) + '\n'); -// } -// } + chunks.push(render_comment_if_needed(type, indent_depth)); + chunks.push(indent + `['${name}']: ` + render_type(type, indent_depth) + '\n'); + } + } -// if (obj.additionalProperties) { -// if (chunks.length) { -// chunks.push('\n'); -// } + if (obj.additionalProperties) { + if (chunks.length) { + chunks.push('\n'); + } -// chunks.push(render_comment_if_needed(obj.additionalProperties, indent_depth)); -// chunks.push(indent + '[*]: ' + render_type(obj.additionalProperties, indent_depth) + '\n'); -// } + chunks.push(render_comment_if_needed(obj.additionalProperties, indent_depth)); + chunks.push(indent + '[*]: ' + render_type(obj.additionalProperties, indent_depth) + '\n'); + } -// return chunks.join(''); -// } + return chunks.join(''); +} -// function render_one_of(obj: JSONSchema, indent_depth = 0, inline = true) { -// const indent = indent_with.repeat(indent_depth); -// const chunks: string[] = [ (inline ? '' : indent) + 'one_of' ]; +function render_one_of(obj: JSONSchema, indent_depth = 0, inline = true) { + const indent = indent_with.repeat(indent_depth); + const chunks: string[] = [ (inline ? '' : indent) + 'one_of' ]; -// chunks.push(' {\n'); -// render_type_list(chunks, obj.oneOf, indent_depth); -// chunks.push(indent + '}'); + chunks.push(' {\n'); + render_type_list(chunks, obj.oneOf, indent_depth); + chunks.push(indent + '}'); -// return chunks.join('') + (inline ? '' : '\n'); -// } + return chunks.join('') + (inline ? '' : '\n'); +} -// function render_all_of(obj: JSONSchema, indent_depth = 0, inline = true) { -// const indent = indent_with.repeat(indent_depth); -// const chunks: string[] = [ (inline ? '' : indent) + 'all_of' ]; +function render_all_of(obj: JSONSchema, indent_depth = 0, inline = true) { + const indent = indent_with.repeat(indent_depth); + const chunks: string[] = [ (inline ? '' : indent) + 'all_of' ]; -// chunks.push(' {\n'); -// render_type_list(chunks, obj.allOf, indent_depth); -// chunks.push(indent + '}'); + chunks.push(' {\n'); + render_type_list(chunks, obj.allOf, indent_depth); + chunks.push(indent + '}'); -// return chunks.join('') + (inline ? '' : '\n'); -// } + return chunks.join('') + (inline ? '' : '\n'); +} -// function render_any_of(obj: JSONSchema, indent_depth = 0, inline = true) { -// const indent = indent_with.repeat(indent_depth); -// const chunks: string[] = [ (inline ? '' : indent) + 'any_of' ]; +function render_any_of(obj: JSONSchema, indent_depth = 0, inline = true) { + const indent = indent_with.repeat(indent_depth); + const chunks: string[] = [ (inline ? '' : indent) + 'any_of' ]; -// chunks.push(' {\n'); -// render_type_list(chunks, obj.anyOf, indent_depth); -// chunks.push(indent + '}'); + chunks.push(' {\n'); + render_type_list(chunks, obj.anyOf, indent_depth); + chunks.push(indent + '}'); -// return chunks.join('') + (inline ? '' : '\n'); -// } + return chunks.join('') + (inline ? '' : '\n'); +} -// function render_not(obj: JSONSchema, indent_depth = 0, inline = true) { -// const indent = indent_with.repeat(indent_depth); -// const prefix = inline ? '' : indent; -// const suffix = inline ? '' : '\n'; -// return prefix + 'not ' + render_type(obj.not, indent_depth, true) + suffix; -// } +function render_not(obj: JSONSchema, indent_depth = 0, inline = true) { + const indent = indent_with.repeat(indent_depth); + const prefix = inline ? '' : indent; + const suffix = inline ? '' : '\n'; + return prefix + 'not ' + render_type(obj.not, indent_depth, true) + suffix; +} -// // ===== Rendering Utils ===== +// ===== Rendering Utils ===== -// function infer_type(obj: JSONSchema) { -// return (obj.properties || obj.additionalProperties || obj.required || 'minimumProperties' in obj || 'maximumProperties' in obj) ? 'object' -// : (obj.items || obj.additionalItems || 'minimumItems' in obj || 'maximumItems' in obj) ? 'array' -// : ('minimum' in obj || 'maximum' in obj || 'exclusiveMinimum' in obj || 'exclusiveMaximum' in obj) ? 'number' -// : ('minLength' in obj || 'maxLength' in obj) ? 'string' -// : null -// ; -// } +function infer_type(obj: JSONSchema) { + return (obj.properties || obj.additionalProperties || obj.required || 'minimumProperties' in obj || 'maximumProperties' in obj) ? 'object' + : (obj.items || obj.additionalItems || 'minimumItems' in obj || 'maximumItems' in obj) ? 'array' + : ('minimum' in obj || 'maximum' in obj || 'exclusiveMinimum' in obj || 'exclusiveMaximum' in obj) ? 'number' + : ('minLength' in obj || 'maxLength' in obj) ? 'string' + : null + ; +} -// function render_type_list(chunks: string[], types: JSONSchemaDefinition[], indent_depth = 0) { -// for (const type of types) { -// if (typeof type === 'object') { -// // skip the extra newline before the first entry -// if (chunks.length > 2) { -// chunks.push('\n'); -// } +function render_type_list(chunks: string[], types: JSONSchema['definitions'][string][], indent_depth = 0) { + for (const type of types) { + if (typeof type === 'object') { + // skip the extra newline before the first entry + if (chunks.length > 2) { + chunks.push('\n'); + } -// chunks.push(render_comment_if_needed(type, indent_depth + 1)); -// chunks.push(render_type(type, indent_depth + 1, false)); -// } -// } -// } + chunks.push(render_comment_if_needed(type, indent_depth + 1)); + chunks.push(render_type(type, indent_depth + 1, false)); + } + } +} -// function render_definitions(obj: JSONSchema, indent_depth = 0) { -// const chunks: string[] = [ ]; -// const indent = indent_with.repeat(indent_depth); +function render_definitions(obj: JSONSchema, indent_depth = 0) { + const chunks: string[] = [ ]; + const indent = indent_with.repeat(indent_depth); -// type Def = Exclude; -// for (const [name, type] of Object.entries(obj.definitions) as [string, Def][]) { -// if (chunks.length) { -// chunks.push('\n'); -// } + type Def = Exclude; + for (const [name, type] of Object.entries(obj.definitions) as [string, Def][]) { + if (chunks.length) { + chunks.push('\n'); + } -// chunks.push(render_comment_if_needed(type, indent_depth)); -// chunks.push(indent + 'def ' + name + ': ' + render_type(type, indent_depth) + '\n'); -// } + chunks.push(render_comment_if_needed(type, indent_depth)); + chunks.push(indent + 'def ' + name + ': ' + render_type(type, indent_depth) + '\n'); + } -// return chunks.join(''); -// } + return chunks.join(''); +} -// function render_inline_list(items: string[], indent_depth = 0) { -// const lines: string[] = [ ]; -// const indent = indent_with.repeat(indent_depth); -// const line_max = 100; -// const first_line_max = 85; +function render_inline_list(items: string[], indent_depth = 0) { + const lines: string[] = [ ]; + const indent = indent_with.repeat(indent_depth); + const line_max = 100; + const first_line_max = 85; -// let line = ''; + let line = ''; -// for (const item of items) { -// if (item.includes('\n')) { -// const sub_lines = item.split('\n'); -// const first_line = sub_lines[0]; -// const last_line = sub_lines[sub_lines.length - 1]; -// const middle_lines = sub_lines.slice(1, -1); + for (const item of items) { + if (item.includes('\n')) { + const sub_lines = item.split('\n'); + const first_line = sub_lines[0]; + const last_line = sub_lines[sub_lines.length - 1]; + const middle_lines = sub_lines.slice(1, -1); -// add_item(first_line); -// lines.push(line + '\n'); + add_item(first_line); + lines.push(line + '\n'); -// for (const item of middle_lines) { -// lines.push(indent + item + '\n'); -// } + for (const item of middle_lines) { + lines.push(indent + item + '\n'); + } -// line = indent + last_line; -// } + line = indent + last_line; + } -// else { -// add_item(item); -// } -// } + else { + add_item(item); + } + } -// function add_item(item: string) { -// const new_length = line.length + item.length + 1; -// const max_length = lines.length ? line_max : first_line_max; + function add_item(item: string) { + const new_length = line.length + item.length + 1; + const max_length = lines.length ? line_max : first_line_max; -// if (line && new_length > max_length) { -// lines.push(line + '\n'); -// line = indent + item; -// } + if (line && new_length > max_length) { + lines.push(line + '\n'); + line = indent + item; + } -// else { -// line += ' ' + item; -// } -// } + else { + line += ' ' + item; + } + } -// if (line) { -// lines.push(line); -// } + if (line) { + lines.push(line); + } -// return lines.join(''); -// } + return lines.join(''); +} -// function str(str: string) { -// return JSON.stringify(str) as `"${string}"`; -// } +function str(str: string) { + return JSON.stringify(str) as `"${string}"`; +} -// function regex(str: string) { -// return "'" + str + "'"; -// } +function regex(str: string) { + return "'" + str + "'"; +} diff --git a/src/json-pointer.ts b/src/json-pointer.ts new file mode 100644 index 0000000..8bf7d6a --- /dev/null +++ b/src/json-pointer.ts @@ -0,0 +1,110 @@ + +// see: https://www.rfc-editor.org/rfc/rfc6901#section-3 +export function escape_for_json_pointer(str: string, escape_for_markdown = true) { + return str + .replace(/~/g, escape_for_markdown ? '\\~0' : '~0') + .replace(/\//g, escape_for_markdown ? '\\~1' : '~1'); +} + +export function unescape_for_json_pointer(str: string) { + return str.replace(/~1/g, '/').replace(/~0/g, '~'); +} + +export interface ResolvedPointer { + value: any; + found: boolean; + stack: { + parent: any; + field: string; + }[]; +} + +export function resolve_json_pointer(root: object, json_pointer: string) : ResolvedPointer { + if (json_pointer === '' || json_pointer === '/') { + return { + value: root, + found: true, + stack: [ ], + }; + } + + if (! json_pointer.startsWith('/')) { + throw new Error('invalid JSON pointer'); + } + + if (json_pointer.includes('//')) { + throw new Error('invalid JSON pointer'); + } + + const fields = json_pointer.split('/').slice(1); + + if (json_pointer.endsWith('/')) { + fields.pop(); + } + + const resolved: ResolvedPointer = { + value: null, + found: true, + stack: [ ], + }; + + let current = root; + + for (let field of fields) { + field = unescape_for_json_pointer(field); + resolved.stack.push({ parent: current, field }); + + if (! (field in current)) { + resolved.found = false; + break; + } + + current = current[field]; + } + + if (resolved.found) { + resolved.value = current; + } + + return resolved; +} + +export function jsonptr(...steps: string[]) { + return new JsonPointer(steps); +} + +export function jsonptr_str(...steps: string[]) { + return (new JsonPointer(steps)).as_str(); +} + +export function jsonptr_md_str(...steps: string[]) { + return (new JsonPointer(steps)).as_md_str(); +} + +export class JsonPointer { + public steps: string[]; + public md_steps: string[]; + + private _str: string; + private _md_str: string; + + constructor(steps: string[] = [ ]) { + this.steps = steps.map((step) => escape_for_json_pointer(step, false)); + this.md_steps = steps.map((step) => escape_for_json_pointer(step, true)); + } + + public as_str() { + return this._str = (this._str || '/' + this.steps.join('/')); + } + + public as_md_str() { + return this._md_str = (this._md_str || '/' + this.md_steps.join('/')); + } + + public step_down(next: string) { + const new_pointer = new JsonPointer(); + new_pointer.steps.push(...this.steps, escape_for_json_pointer(next, false)); + new_pointer.md_steps.push(...this.md_steps, escape_for_json_pointer(next, true)); + return new_pointer; + } +} diff --git a/src/markdown-builder.ts b/src/markdown-builder.ts new file mode 100644 index 0000000..1059723 --- /dev/null +++ b/src/markdown-builder.ts @@ -0,0 +1,317 @@ + +import { JsonPointer } from './json-pointer'; +import { SectionIds } from './section-ids'; + +export interface MarkdownBuilderOptions { + numbered_headings?: boolean | { + prefix?: string; + }; +} + +export type Content = string | (() => string); + +export type HeadingDepth = 1 | 2 | 3 | 4 | 5 | 6; + +const max_section_depth = 5; + +export interface TableOfContentsItem { + id: string | JsonPointer; + label: string; + depth: HeadingDepth; +} + +export interface AttrsOptions { + id?: string | JsonPointer; + classes?: string[]; + attrs?: Record; +} + +export interface HeadingOptions extends AttrsOptions { + /** If set to true, this heading will not show up in the table of contents */ + no_table_of_contents?: boolean; +} + +export interface LinkOptions extends AttrsOptions { + text?: string; +} + +export interface CodeBlockOptions { + lang?: string; + caption?: string; + extra_depth?: number; +} + +export class MarkdownBuilder { + private ids?: SectionIds; + private contents: Content[] = [ ]; + private toc_items: TableOfContentsItem[] = [ ]; + private remaining_section_depth = max_section_depth; + + constructor( + private readonly options: MarkdownBuilderOptions = { } + ) { + if (options.numbered_headings) { + if (typeof options.numbered_headings === 'object') { + this.ids = new SectionIds(options.numbered_headings.prefix || 'section-'); + } + } + } + + public as_str() { + return this.contents.map((content) => { + return typeof content === 'function' ? content() : content; + }).join(''); + } + + + + // ===== + + public raw(raw: string) { + this.contents.push(raw); + } + + public text(text: string) { + // todo: escape text + this.contents.push(text); + } + + + + // ===== Block ===== + + public p(text: string) { + return this.raw(text + '\n\n'); + } + + public h1(text: string, opts: HeadingOptions = { }) { + return this.heading(1, text, opts); + } + + public h2(text: string, opts: HeadingOptions = { }) { + return this.heading(2, text, opts); + } + + public h3(text: string, opts: HeadingOptions = { }) { + return this.heading(3, text, opts); + } + + public h4(text: string, opts: HeadingOptions = { }) { + return this.heading(4, text, opts); + } + + public h5(text: string, opts: HeadingOptions = { }) { + return this.heading(5, text, opts); + } + + public h6(text: string, opts: HeadingOptions = { }) { + return this.heading(6, text, opts); + } + + public heading(level: 1 | 2 | 3 | 4 | 5 | 6, text: string, { id, classes, no_table_of_contents }: HeadingOptions) { + let label = text; + + if (! id && this.ids) { + let sec: string; + ({ id, sec } = this.ids.next(level, text)); + label = `${sec}. ${label}`; + } + + let rendered = `${'#'.repeat(level)} ${text}${this.attrs_md({ id, classes })}\n\n`; + + if (! no_table_of_contents) { + this.toc_items.push({ + id, + depth: level, + label: text, + }); + } + + this.raw(rendered); + } + + public hr() { + this.raw('---\n\n'); + } + + + + // ===== Open Blocks (closed separately) ===== + + public section(opts: AttrsOptions = { }) { + const depth = this.remaining_section_depth--; + + if (! depth) { + this.remaining_section_depth++; + throw new Error(`max section depth (${max_section_depth}) exceeded`); + } + + const fence = '!'.repeat(depth + 5); + const attrs = this.attrs_md(opts); + + this.raw(`${fence}${attrs}\n`); + + let closed = false; + return () => { + if (closed) { + throw new Error('attempted to close section twice'); + } + + closed = true; + this.raw(fence + '\n\n'); + this.remaining_section_depth++; + }; + } + + public code_block(opts: CodeBlockOptions = { }) { + const fence = '`'.repeat(3 + (opts.extra_depth || 0)); + const caption = opts.caption ? ' ' + JSON.stringify(opts.caption) : ''; + + this.raw(`${fence}${opts.lang || 'plain'}${caption}\n`); + + let closed = false; + return () => { + if (closed) { + throw new Error('attempted to close code block twice'); + } + + closed = true; + this.raw(fence + '\n\n'); + }; + } + + + + // ===== Inline ===== + + public code(text: string) { + return `\`${text.replace(/`/g, '\\`')}\``; + } + + public math(text: string) { + return `$${text}$`; + } + + public link(url: string, opts: LinkOptions = { }) { + if (! opts.text) { + return `<${url}>`; + } + + const attrs = this.attrs_md(opts); + + if (! attrs) { + return `[${opts.text}](${url})`; + } + + // todo: more links + } + + public em(text: string) { + return `_${text.replace(/_/g, '\\_')}_`; + } + + + + + + // ===== Complex ===== + + public cite() { + // todo: citations/footnotes + } + + public table_of_contents() { + return () => { + return this.toc_items.map(({ id, depth, label }) => { + return ' '.repeat(depth - 1) + `[${label}](#${id})`; + }).join('\n') + '\n'; + }; + } + + public dl(opts: AttrsOptions = { }) { + const attrs = this.attrs_html(opts); + this.raw(`\n`); + + let closed = false; + return () => { + if (closed) { + throw new Error('attempted to close
twice'); + } + + closed = true; + this.raw('
\n\n'); + }; + } + + public dt(text: string, opts: AttrsOptions = { }) { + const attrs = this.attrs_html(opts); + this.raw(`${text}`); + } + + public dd(text: string, opts: AttrsOptions = { }) { + const attrs = this.attrs_html(opts); + this.raw(`${text}\n`); + } + + + + // ===== + + private attrs_md(raw_attrs: AttrsOptions) { + const attrs: string[] = [ ]; + + if (raw_attrs.id) { + attrs.push('#' + md_str(raw_attrs.id)); + } + + if (raw_attrs.classes) { + for (const classname of raw_attrs.classes) { + attrs.push('.' + classname); + } + } + + if (raw_attrs.attrs) { + for (const [name, value] of Object.entries(raw_attrs.attrs)) { + attrs.push(`:${name}=${value}`); + } + } + + return attrs.length ? ` {${attrs.join(' ')}}` : ''; + } + + private attrs_html(raw_attrs: AttrsOptions) { + const attrs: string[] = [ ]; + + if (raw_attrs.id) { + attrs.push(`id="${str(raw_attrs.id)}"`); + } + + if (raw_attrs.classes) { + attrs.push(`class="${raw_attrs.classes.join(' ')}"`); + } + + if (raw_attrs.attrs) { + for (const [name, value] of Object.entries(raw_attrs.attrs)) { + attrs.push(`${name}="${value}"`); + } + } + + return attrs.length ? ` ${attrs.join(' ')}` : ''; + } +} + +function md_str(str: string | JsonPointer) : string { + if (str instanceof JsonPointer) { + return str.as_md_str(); + } + + return str; +} + +function str(str: string | JsonPointer) : string { + if (str instanceof JsonPointer) { + return str.as_str(); + } + + return str; +} diff --git a/src/section-ids.ts b/src/section-ids.ts new file mode 100644 index 0000000..ad4d1e2 --- /dev/null +++ b/src/section-ids.ts @@ -0,0 +1,25 @@ + +export const char_section = 'ยง'; + +export class SectionIds { + public current: number[] = [ ]; + public contents: { id: string, label: string }[] = [ ]; + + constructor( + public prefix = '' + ) { } + + public next(depth: number, label: string) { + while (depth > this.current.length) { + this.current.push(0); + } + + this.current.length = depth; + this.current[depth - 1]++; + + const sec = this.current.join('.'); + const id = this.prefix + sec; + this.contents.push({ id, label }); + return { id, sec }; + } +}