diff --git a/astro.config.mjs b/astro.config.mjs index 02ec23fb83..8865da4e7b 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -3,12 +3,9 @@ import starlight from '@astrojs/starlight'; import { rehypeHeadingIds } from '@astrojs/markdown-remark'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import locales from './locales.json'; -import configGenerator from './src/plugins/configGenerator'; import starlightLinksValidator from 'starlight-links-validator'; import starlightBlog from 'starlight-blog'; -await configGenerator(); - const authors = { nothingismagick: { name: 'Daniel Thompson-Yvetot', diff --git a/package.json b/package.json index c18ebd3769..2e40263e11 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,10 @@ "format": "prettier -w --cache --plugin prettier-plugin-astro .", "build:reference": "pnpm --filter js-api-generator run build", "build:releases": "pnpm --filter releases-generator run build", + "build:config": "pnpm --filter config-generator run build", "build:astro": "astro build", "build:i18n": "pnpm --filter docs-i18n-tracker run build", - "build": "pnpm dev:setup && pnpm build:reference && pnpm build:releases && pnpm build:astro && pnpm build:i18n", + "build": "pnpm dev:setup && pnpm build:reference && pnpm build:config && pnpm build:releases && pnpm build:astro && pnpm build:i18n", "preview": "astro preview" }, "dependencies": { diff --git a/packages/config-generator/build.ts b/packages/config-generator/build.ts new file mode 100644 index 0000000000..01a4659199 --- /dev/null +++ b/packages/config-generator/build.ts @@ -0,0 +1,498 @@ +import { JSONSchema7, JSONSchema7Definition, JSONSchema7TypeName } from 'json-schema'; +import { existsSync, writeFileSync } from 'node:fs'; +import { slug } from 'github-slugger'; + +const schemaFile = '../tauri/core/tauri-config-schema/schema.json'; +const outputFile = '../../src/content/docs/2/reference/config.md'; + +if (!existsSync(schemaFile)) { + throw Error('Could not find the Tauri config schema. Is the Tauri submodule initialized?'); +} + +let schema: JSONSchema7 = (await import(schemaFile)).default; + +const output = [ + '---\n# NOTE: This file is auto-generated in packages/config-generator/build.ts\n# For corrections please edit https://github.com/tauri-apps/tauri/blob/dev/core/tauri-utils/src/config.rs directly\n\ntitle: Configuration\n---', +]; + +output.push( + ...buildSchemaDefinition(schema, { + headingLevel: 2, + renderTitle: false, + }) +); + +writeFileSync(outputFile, output.join('\n\n')); + +interface Options { + headingLevel: number; + renderTitle: boolean; + leadWithType: boolean; +} + +function buildSchemaDefinition( + schema: JSONSchema7Definition, + passedOptions: Partial = {} +): string[] { + // Note: $id, $schema, and $comment are explicitly not rendered + + // Assign default values for any missing options + const opts = Object.assign( + { + headingLevel: 1, + renderTitle: true, + leadWithType: false, + }, + passedOptions + ); + + if (typeof schema === 'boolean') { + return [`\`${schema}\``]; + } + + const out: string[] = []; + + out.push(...buildType(schema, opts)); + out.push(...buildExtendedItems(schema, opts)); + out.push(...buildConditionalSubschemas(schema, opts)); + out.push(...buildBooleanSubschemas(schema, opts)); + out.push(...buildMetadata(schema, opts)); + out.push(...buildProperties(schema, opts)); + out.push(...buildExtendedMetadata(schema, opts)); + out.push(...buildDefinitions(schema, opts)); + + return out; +} + +/** + * Builds: title, readOnly, writeOnly, description + * + * Also see: buildExtendedMetadata() + */ +function buildMetadata(schema: JSONSchema7Definition, opts: Options): string[] { + if (typeof schema !== 'object') { + return []; + } + + const out: string[] = []; + + opts.renderTitle && + schema.title && + out.push(`${'#'.repeat(Math.min(opts.headingLevel, 6))} ${schema.title}`); + + if (schema.readOnly || schema.writeOnly) { + const line = []; + schema.readOnly && line.push('Read only'); + schema.writeOnly && line.push('Write only'); + out.push(line.join(' & ')); + } + + schema.description && + out.push( + schema.description + // Set headings to appropriate level + .replaceAll(/#{1,6}(?=.+[\n\\n])/g, '#'.repeat(Math.min(opts.headingLevel + 1, 6))) + // Fix improperly formatted heading links + .replaceAll(/#{1,6}(?=[^\s#])/g, '#') + .replaceAll('<', '<') + .replaceAll('>', '>') + // Fix for link at https://github.com/tauri-apps/tauri/blob/713f84db2b5bf17e4217053a229f9c11cbb22c74/core/tauri-config-schema/schema.json#L1863-L1864 + .replace('#SecurityConfig.devCsp', '#securityconfig') + ); + + return out; +} + +/** + * Builds: $ref, type, enum, const, items + * Number validation: multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum + * String validation: maxLength, minLength, pattern + * Array validation: maxItems, minItems, uniqueItems + * Object validation: maxProperties, minProperties + * Semantic validation: format + * Non-JSON data validation: contentMediaType, contentEncoding + */ +function buildType(schema: JSONSchema7Definition, opts: Options): string[] { + if (typeof schema !== 'object') { + return []; + } + + let line: string = ''; + + if (schema.type) { + if (Array.isArray(schema.type)) { + line += schema.type.map((value) => buildTypeName(value, schema, opts)).join(' | '); + } else { + line += buildTypeName(schema.type, schema, opts); + } + } + + if (schema.$ref) { + const reference = schema.$ref.split('/').pop(); + + if (!reference) { + throw Error(`Invalid reference: ${schema.$ref}`); + } + + line += `[\`${reference}\`](#${slug(reference)})`; + } + + if (schema.const) { + switch (typeof schema.const) { + case 'string': + line += `\`"${schema.const}"\``; + break; + default: + line += `\`${schema.const}\``; + } + } + + if (schema.enum) { + const enumValues: string[] = []; + schema.enum.forEach((value) => { + switch (typeof value) { + case 'string': + enumValues.push(`\`"${value}"\``); + break; + default: + enumValues.push(`\`${value}\``); + } + }); + line += enumValues.join(' | '); + } + + const validation = []; + + // Number validation + schema.multipleOf && validation.push(`multiple of \`${schema.multipleOf}\``); + schema.maximum && validation.push(`maximum of \`${schema.maximum}\``); + schema.exclusiveMaximum && validation.push(`exclusive maximum of \`${schema.exclusiveMaximum}\``); + schema.minimum && validation.push(`minimum of \`${schema.minimum}\``); + schema.exclusiveMinimum && validation.push(`exclusive minimum of \`${schema.exclusiveMinimum}\``); + + // String validation + schema.maxLength && validation.push(`maximum length of \`${schema.maxLength}\``); + schema.minLength && validation.push(`minimum length of \`${schema.minLength}\``); + schema.pattern && validation.push(`pattern of \`${schema.pattern}\``); + + // Array validation + schema.maxItems && validation.push(`maximum of \`${schema.maxItems}\` items`); + schema.minItems && validation.push(`minimum of \`${schema.minItems}\` items`); + schema.uniqueItems && validation.push(`each item must be unique`); + + // Object validation + schema.maxProperties && validation.push(`maximum of \`${schema.maxProperties}\` properties`); + schema.minProperties && validation.push(`minimum of \`${schema.minProperties}\` properties`); + + // Semantic validation + schema.format && validation.push(`formatted as \`${schema.format}\``); + + // Non-JSON data validation + schema.contentMediaType && + validation.push(`content media type of \`${schema.contentMediaType}\``); + schema.contentEncoding && validation.push(`content encoding of \`${schema.contentEncoding}\``); + + if (validation.length > 0) { + line += ' ' + validation.join(', '); + } + + return [line]; + + function buildTypeName( + typeName: JSONSchema7TypeName, + parentSchema: JSONSchema7Definition, + opts: Options + ): string { + // Rendering logic for enums and consts are handled separately + if (typeof parentSchema === 'object' && (parentSchema.enum || parentSchema.const)) { + return ''; + } + + let line = ''; + switch (typeName) { + case 'object': + break; + case 'array': + if (typeof parentSchema !== 'object' || !parentSchema.items) { + throw Error('Invalid array'); + } + if (Array.isArray(parentSchema.items)) { + line += parentSchema.items + .map((value) => { + const definition = buildSchemaDefinition(value, opts); + if (definition.length > 1) { + // Format additional information to be in parenthesis + const [first, ...rest] = definition; + return [first, ' (', rest.join(', '), ')']; + } else { + return definition.join(); + } + }) + .join(' | '); + } else { + line += buildSchemaDefinition(parentSchema.items, opts); + } + line += '[]'; + break; + default: + line += '`' + typeName + '`'; + } + return line; + } +} + +/** + * Builds: additionalItems, contains + */ +function buildExtendedItems(schema: JSONSchema7Definition, opts: Options): string[] { + if (typeof schema !== 'object') { + return []; + } + + if (schema.additionalItems) { + throw Error('Not implemented'); + } + if (schema.contains) { + throw Error('Not implemented'); + } + + const out: string[] = []; + + schema.additionalItems && out.push(`additionalItems: ${JSON.stringify(schema.additionalItems)}`); + schema.contains && out.push(`contains: ${JSON.stringify(schema.contains)}`); + + return out; +} + +/** + * Builds: required, properties, patternProperties, additionalProperties, dependencies, propertyNames + */ +function buildProperties(schema: JSONSchema7Definition, opts: Options): string[] { + if (typeof schema !== 'object') { + return []; + } + + const out: string[] = []; + + if (schema.properties) { + out.push(`**Object Properties**:`); + + const properties = Object.entries(schema.properties) + .filter(([key]) => key !== '$schema') + .sort(([a], [b]) => a.localeCompare(b)); + + out.push( + properties + .map( + ([key]) => + `- ${key} ${schema.required && schema.required.includes(key) ? '(required)' : ''}` + ) + .join('\n') + ); + + properties.forEach(([key, value]) => { + out.push(`${'#'.repeat(Math.min(6, opts.headingLevel + 1))} ${key}`); + out.push(...buildSchemaDefinition(value, { ...opts, headingLevel: opts.headingLevel + 1 })); + }); + } + + schema.additionalProperties && + out.push( + `**Allows additional properties**: ${buildSchemaDefinition( + schema.additionalProperties, + opts + )}` + ); + + if (schema.patternProperties || schema.dependencies || schema.propertyNames) { + throw Error('Not implemented'); + } + + schema.patternProperties && + out.push(`patternProperties: ${JSON.stringify(schema.patternProperties)}`); + schema.dependencies && out.push(`dependencies: ${JSON.stringify(schema.dependencies)}`); + schema.propertyNames && out.push(`propertyNames: ${JSON.stringify(schema.propertyNames)}`); + + return out; +} + +/** + * Builds: if, then, else + */ +function buildConditionalSubschemas(schema: JSONSchema7Definition, opts: Options): string[] { + if (typeof schema !== 'object') { + return []; + } + + const out: string[] = []; + + if (schema.if || schema.then || schema.else) { + throw Error('Not implemented'); + } + + schema.if && out.push(`if: ${JSON.stringify(schema.if)}`); + schema.then && out.push(`then: ${JSON.stringify(schema.then)}`); + schema.else && out.push(`else: ${JSON.stringify(schema.else)}`); + + return out; +} + +/** + * Builds: allOf, anyOf, oneOf, not + */ +function buildBooleanSubschemas(schema: JSONSchema7Definition, opts: Options): string[] { + if (typeof schema !== 'object') { + return []; + } + + const out: string[] = []; + + if (schema.allOf) { + out.push(...buildXOf('All', schema.allOf, opts)); + } + + if (schema.anyOf) { + out.push(...buildXOf('Any', schema.anyOf, opts)); + } + + if (schema.oneOf) { + out.push(...buildXOf('One', schema.oneOf, opts)); + } + + if (schema.not) { + throw Error('Not implemented'); + } + + schema.not && out.push(`not: ${JSON.stringify(schema.not)}`); + + return out; + + function buildXOf( + label: 'One' | 'Any' | 'All', + schemas: JSONSchema7Definition[], + opts: Options + ): string[] { + const definitions = schemas.map((value) => + buildSchemaDefinition(value, { ...opts, leadWithType: true }) + ); + + if (definitions.every((definition) => definition.length == 1)) { + // Short definition, can be rendered on a single line + return [definitions.join(' | ')]; + } + + const out: string[] = []; + + if (definitions.length > 1) { + // Render as a list + out.push(`**${label} of the following**:`); + const list: string[] = []; + + const additionalDefinitions: string[][] = []; + + definitions.forEach((definition) => { + if (definition[0].startsWith('`object`')) { + // Is an object, need to render subdefinitions + const [first] = definition; + list.push(`- ${first}: Subdefinition ${additionalDefinitions.length + 1}`); + additionalDefinitions.push(definition); + } else { + // Render inline in list + list.push(`- ${definition.map((value) => value.replaceAll('\n', ' ')).join(' ')}`); + } + }); + + out.push(list.join('\n')); + + if (additionalDefinitions.length > 0) { + out.push(`${'#'.repeat(Math.min(6, opts.headingLevel))} Subdefinitions`); + additionalDefinitions.forEach((definition, index) => { + // Render heading + out.push(`${'#'.repeat(Math.min(6, opts.headingLevel + 1))} Subdefinition ${index + 1}`); + + // Render definition, giving additional heading indention as needed + definition.forEach((line) => { + out.push( + line.replaceAll(/#{1,6}\s(?=.+)/g, '#'.repeat(Math.min(opts.headingLevel + 2, 6))) + ); + }); + }); + } + } else { + // Render as a block + // Mapping might need some fixing... + out.push(...definitions.map((definition) => definition.join())); + } + return out; + } +} + +/** + * Builds: default, examples + * + * https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00#section-8.2.4 + * https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-9 + */ +function buildExtendedMetadata(schema: JSONSchema7Definition, opts: Options): string[] { + if (typeof schema !== 'object') { + return []; + } + + const out: string[] = []; + + if (schema.default) { + if (typeof schema.default === 'string') { + out.push(`**Default**: \`"${schema.default}"\``); + } else if (typeof schema.default !== 'object') { + // Render on single line + out.push(`**Default**: \`${schema.default}\``); + } else if (Object.keys(schema.default).length == 0) { + // Empty object, render on a single line + out.push(`**Default**: \`${JSON.stringify(schema.default)}\``); + } else { + // Object with properties, render in code block + out.push( + `\n\`\`\`json title='Default'\n${JSON.stringify(schema.default, null, '\t')}\n\`\`\`\n` + ); + } + } + + if (schema.examples) { + throw Error('Examples not implemented'); + } + + return out; +} + +/** + * Builds: $defs, definitions + * + * https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00#section-8.2.4 + * https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-9 + */ +function buildDefinitions(schema: JSONSchema7Definition, opts: Options): string[] { + if (typeof schema !== 'object') { + return []; + } + + const out: string[] = []; + + // Combine definitions together + // `definitions` was renamed to `$defs` in Draft 7 but still allowed in either place + const definitions = { ...schema.$defs, ...schema.definitions }; + if (Object.keys(definitions).length > 0) { + out.push(`${'#'.repeat(Math.min(opts.headingLevel, 6))} Definitions`); + Object.entries(definitions) + .sort(([a], [b]) => a.localeCompare(b)) + .forEach(([key, value]) => { + out.push(`${'#'.repeat(Math.min(opts.headingLevel, 6) + 1)} ${key}`); + out.push( + ...buildSchemaDefinition(value, { + ...opts, + headingLevel: Math.min(opts.headingLevel, 6) + 2, + }) + ); + }); + } + return out; +} diff --git a/packages/config-generator/package.json b/packages/config-generator/package.json new file mode 100644 index 0000000000..aec5379a46 --- /dev/null +++ b/packages/config-generator/package.json @@ -0,0 +1,21 @@ +{ + "name": "config-generator", + "version": "1.0.0", + "private": "true", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "build": "tsm ./build.ts" + }, + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "@types/node": "^20.10.0", + "github-slugger": "^2.0.0", + "tsm": "^2.3.0", + "typescript": "^5.3.2" + } +} diff --git a/packages/config-generator/tsconfig.json b/packages/config-generator/tsconfig.json new file mode 100644 index 0000000000..e70b1b2dab --- /dev/null +++ b/packages/config-generator/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "ES2021", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/packages/js-api-generator/package.json b/packages/js-api-generator/package.json index 197354efc1..201640ad61 100644 --- a/packages/js-api-generator/package.json +++ b/packages/js-api-generator/package.json @@ -8,7 +8,8 @@ }, "keywords": [], "author": "", - "license": "ISC", + "license": "MIT", + "private": "true", "dependencies": { "github-slugger": "^2.0.0", "tsm": "^2.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bd45c51c8..108330d26a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,24 @@ importers: specifier: ^0.4.2 version: 0.4.2(@astrojs/starlight@0.13.0)(astro@3.6.4) + packages/config-generator: + dependencies: + '@types/json-schema': + specifier: ^7.0.15 + version: 7.0.15 + '@types/node': + specifier: ^20.10.0 + version: 20.10.5 + github-slugger: + specifier: ^2.0.0 + version: 2.0.0 + tsm: + specifier: ^2.3.0 + version: 2.3.0 + typescript: + specifier: ^5.3.2 + version: 5.3.2 + packages/i18n-tracker: devDependencies: '@types/html-escaper': @@ -1161,6 +1179,12 @@ packages: resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} dev: false + /@types/node@20.10.5: + resolution: {integrity: sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==} + dependencies: + undici-types: 5.26.5 + dev: false + /@types/parse5@6.0.3: resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} dev: false @@ -1172,7 +1196,7 @@ packages: /@types/sax@1.2.7: resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} dependencies: - '@types/node': 17.0.45 + '@types/node': 20.10.5 dev: false /@types/unist@2.0.10: @@ -4877,6 +4901,10 @@ packages: hasBin: true dev: false + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: false + /unherit@3.0.1: resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==} dev: false diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d95df763f7..46e2ae91db 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - 'packages/i18n-tracker' - 'packages/js-api-generator' + - 'packages/config-generator' - 'packages/tauri-typedoc-theme' - 'packages/releases-generator' diff --git a/src/plugins/configGenerator.ts b/src/plugins/configGenerator.ts deleted file mode 100644 index c2de3d9ad7..0000000000 --- a/src/plugins/configGenerator.ts +++ /dev/null @@ -1,470 +0,0 @@ -import fs from 'node:fs'; - -export default async function configGenerator() { - if (fs.existsSync('packages/tauri/tooling/api/node_modules')) { - const schemaPath = 'packages/tauri/core/tauri-config-schema/schema.json'; - const schemaString = fs - .readFileSync(schemaPath) - .toString() - // Fixes any angle brackets that aren't escaped propertly - .replaceAll('(?', '>'); - const schema = JSON.parse(schemaString); - const targetPath = 'src/content/docs/2/reference/config.md'; - const nullMarkdown = '_null_'; - - const header = `--- -title: Tauri Configuration ---- - -`; - - const builtDefinitions = []; - - function buildObject(key, value, headerLevel) { - let out = []; - headerLevel = Math.min(headerLevel, 6); - - var headerTitle = value.title ? 'Configuration' : key; - - var header = `${'#'.repeat(headerLevel)} ${headerTitle}\n`; - - if (headerLevel === 1) { - headerLevel = 2; - } - - out.push(header); - out.push(`${descriptionConstructor(value.description)}\n`); - - out.push(`${longFormTypeConstructor(key, value, headerLevel)}\n`); - out = out.concat(buildProperties(headerTitle, value, headerLevel)); - - out = out.concat(inspectRef(value, headerLevel + 1)); - - return out; - } - - function buildDef(name, headerLevel) { - const def = name.replace('#/definitions/', ''); - if (!builtDefinitions.includes(def)) { - builtDefinitions.push(def); - const obj = schema.definitions[def]; - return buildObject(def, obj, headerLevel); - } - return []; - } - - function inspectRef(object, headerLevel) { - let out = []; - - if (object.$ref) { - out = out.concat(buildDef(object.$ref, headerLevel)); - } - - if (object.additionalProperties && object.additionalProperties.$ref) { - out = out.concat(buildDef(object.additionalProperties.$ref, headerLevel)); - } - - if (object.items && object.items.$ref) { - out = out.concat(buildDef(object.items.$ref, headerLevel)); - } - - for (const opt of object.allOf || []) { - out = out.concat(inspectRef(opt, headerLevel)); - } - - for (const opt of object.anyOf || []) { - out = out.concat(inspectRef(opt, headerLevel)); - } - - return out; - } - - function buildProperties(parentName, object, headerLevel) { - const out = []; - if (!object.properties) return out; - - const required = object.required || []; - - // Build table header - out.push('| Name | Type | Default | Description |'); - out.push('| ---- | ---- | ------- | ----------- |'); - - let definitions = []; - - // Populate table - Object.entries(object.properties).forEach(([key, value]) => { - if (key == '$schema') return; - - let propertyType = typeConstructor(value, true); - let propertyDefault = defaultConstructor(value); - - if (required.includes(key)) { - propertyType += ' (required)'; - if (propertyDefault === nullMarkdown) { - propertyDefault = ''; - } - } - - const url = `${parentName.toLowerCase()}.${key.toLowerCase()}`; - const name = `
\`${key}\`
`; - out.push( - `| ${name} | ${propertyType} | ${propertyDefault} | ${descriptionConstructor( - value.description, - true - )} |` - ); - - definitions = definitions.concat(inspectRef(value, headerLevel + 1)); - }); - - out.push('\n'); - - return out.concat(definitions); - } - - function descriptionConstructor(description, fixNewlines = false) { - if (!description) return; - - // Remove links to current page - description = description.replaceAll( - /\n\nSee more: https:\/\/tauri\.app\/v[0-9]\/api\/config.*$/g, - '' - ); - - // fix Rust doc style links - description = description.replaceAll(/\[`Self::(\S+)`\]/g, '`$1`'); - - // Fix bullet points not being on a newline - description = description.replaceAll(' - ', '\n- '); - - // Parse any json code blocks - if (description.includes('```json ')) { - let newDescription = ''; - const s = description.split('```'); - - for (const text of s) { - if (text.startsWith('json')) { - const description = text.match(/([^{]+)/)[0]; - const json = JSON.stringify(JSON.parse(text.replace(description, '')), null, 2); - newDescription += `${description}\n${json}\n`; - } else { - newDescription += text + '```'; - } - } - description = newDescription; - } - - const referenceStyleLinksRegex = /(\[[A-Za-z0-9 ]+\]): (.+)/g; - const referenceStyleLinksMatches = referenceStyleLinksRegex.exec(description); - if (referenceStyleLinksMatches) { - let link = referenceStyleLinksMatches[2]; - // strip `<` and `>` from `<$url>` - if (link.startsWith('<')) { - link = link.substring(1, link.length - 1); - } - description = description - .replace(referenceStyleLinksMatches[0], '') - .replace(referenceStyleLinksMatches[1], `${referenceStyleLinksMatches[1]}(${link})`) - .trim(); - } - - // Fix any embedded new lines - if (fixNewlines) { - description = description.replaceAll('\n', '
'); - } - - const markdownLinkRegex = /\[([^\[]+)\]\((.*)\)/gm; - const markdownLinkMatches = markdownLinkRegex.exec(description); - - if (markdownLinkMatches) { - const url = markdownLinkMatches[2]; - if (!url.startsWith('http')) { - description = description.replace(url, url.toLowerCase().replaceAll('_', '')); - } - } - return description; - } - - function typeConstructor(object, describeObject = false) { - const canBeNull = - (object.type && object.type.includes('null')) || - (object.anyOf && object.anyOf.some((item) => item.type === 'null')); - - if (object.$ref) { - return refLinkConstructor(object.$ref, canBeNull); - } - - if (object.additionalProperties && object.additionalProperties.$ref) { - return refLinkConstructor(object.additionalProperties.$ref, canBeNull); - } - - if (object.items && object.items.$ref) { - return refLinkConstructor(object.items.$ref, canBeNull); - } - - if (object.anyOf) { - // Removes any null values - const items = object.anyOf.filter((item) => !(item.type && item.type == 'null')); - - if (canBeNull && items.length == 1) { - return `${items.map((t) => typeConstructor(t, describeObject))}?`; - } - - return items.map((t) => typeConstructor(t, describeObject)).join(' \\| '); - } - - if (object.allOf) { - return refLinkConstructor(object.allOf[0].$ref); - } - - if (object.oneOf) { - return object.oneOf.map((t) => typeConstructor(t, describeObject)).join(' | '); - } - - const m = describeObject ? '' : '`'; - - if (object.type) { - var typeString = ''; - - // See what the type is - switch (typeof object.type) { - case 'string': - // See if referencing a different type - switch (object.type) { - case 'string': - typeString = object.enum - ? object.enum.map((e) => `"${e}"`).join(', ') - : `${m}${object.type}${m}`; - break; - case 'number': - case 'integer': - case 'boolean': - typeString = `${m}${object.type}${m}`; - break; - case 'object': - if (describeObject && object.properties) { - typeString = `${m}{`; - const len = Object.keys(object.properties).length; - let i = 0; - for (const prop in object.properties) { - typeString += ` "${prop}": ${typeConstructor( - object.properties[prop], - describeObject - )}`; - i++; - if (i < len) typeString += ','; - } - typeString += ` }${m}`; - } else { - typeString = `${m}${object.type}${m}`; - } - break; - case 'array': - if (object.items) { - if (describeObject) { - typeString = `${typeConstructor(object.items, describeObject)}[]`; - } else { - const type = typeConstructor(object.items, true); - const hasLink = type.includes('(#'); - typeString = hasLink - ? type.replace(/\[`(.*)`\]/, '[`$1[]`]') - : `${m}${type}[]${m}`; - } - break; - } - default: - break; - } - break; - case 'undefined': - typeString = nullMarkdown; - break; - case 'object': - if (Array.isArray(object.type)) { - // Check if it should just be an optional value - if (object.type.length == 2 && object.type.includes('null')) { - typeString = `${m}${object.type.filter((item) => item != 'null')}${m}?`; - break; - } - } - default: - break; - } - - var additionalProperties = []; - - if (object.format) { - additionalProperties.push(`format: \`${object.format}\``); - } - - if (object.multipleOf) { - additionalProperties.push(`multiple of: \`${object.multipleOf}\``); - } - - if (object.minimum) { - additionalProperties.push(`minimum: \`${object.minimum}\``); - } - - if (object.exclusiveMinimum) { - additionalProperties.push(`exclusive minimum: \`${object.exclusiveMinimum}\``); - } - - if (object.maximum) { - additionalProperties.push(`maximum: \`${object.maximum}\``); - } - - if (object.exclusiveMaximum) { - additionalProperties.push(`exclusive maximum: \`${object.exclusiveMaximum}\``); - } - - if (typeString != '') { - if (additionalProperties.length > 0) { - const props = `_(${additionalProperties.join(', ')})_`; - return `${typeString} ${props}`; - } - return typeString; - } - } - - if (object.enum) { - return `${m}${object.enum.map((e) => `"${e}"`).join(', ')}${m}`; - } - - if (Array.isArray(object)) { - if (describeObject) { - const type = []; - for (const obj of object) { - type.push(typeConstructor(obj)); - } - if (type.every((t) => t === type[0])) { - return `${m}${type[0]}${m}`; - } - return `${m}[${type.join(', ')}]${m}`; - } else { - return `${m}array${m}`; - } - } - - console.log('A type was not able to be parsed:', object); - return JSON.stringify(object); - } - - /** prepares a description to be added to a markdown bullet point list */ - function listDescription(description) { - return description.replace('\n\n', '\n\n\t'); - } - - function longFormTypeConstructor(key, object, headerLevel) { - if (object.enum) { - var buffer = []; - buffer.push(`Can be any of the following \`${object.type}\` values:`); - object.enum.forEach((item) => { - buffer.push(`- ${item}`); - }); - return buffer.join('\n'); - } - if (object.anyOf) { - var buffer = []; - buffer.push('Can be any of the following types:\n'); - object.anyOf.forEach((item) => { - var description = ':'; - if (item.description) { - description = `: ${descriptionConstructor(item.description)}`; - } - const hasProperties = 'properties' in item; - let typeDef = typeConstructor(item, hasProperties); - if (hasProperties) { - typeDef = '`' + typeDef + '`'; - } - buffer.push(`- ${typeDef}${listDescription(description)}`); - if (hasProperties) { - buffer.push('\n\t'); - buffer.push( - buildProperties(key, item, headerLevel) - .map((line) => `\t${line}`) - .join('\n') - ); - } - }); - - return buffer.join(`\n`); - } - - if (object.oneOf) { - var buffer = []; - buffer.push('Can be any **ONE** of the following types:\n'); - object.oneOf.forEach((item) => { - var description = ':'; - if (item.description) { - description = `: ${descriptionConstructor(item.description)}`; - } - const hasProperties = 'properties' in item; - let typeDef = typeConstructor(item, hasProperties); - if (hasProperties) { - typeDef = '`' + typeDef + '`'; - } - buffer.push(`- ${typeDef}${listDescription(description)}`); - if ('properties' in item) { - buffer.push('\n\t'); - buffer.push( - buildProperties(key, item, headerLevel) - .map((line) => `\t${line}`) - .join('\n') - ); - } - }); - - return buffer.join(`\n`); - } - - return `Type: ${typeConstructor(object)}`; - } - - function defaultConstructor(object) { - switch (typeof object.default) { - case 'boolean': - case 'number': - return `\`${object.default}\``; - case 'object': - // Check if empty array - if (Array.isArray(object.default) && object.default.length == 0) { - return '[]'; - } - default: - } - - if (object.$ref) { - console.error('Found $ref default:', object.$ref); - } - - if (object.anyOf) { - const link = object.anyOf[0].$ref.replace('#/definitions/', '').toLowerCase(); - return `[view](#${link})`; - } - - if (object.allOf) { - const link = object.allOf[0].$ref.replace('#/definitions/', '').toLowerCase(); - return `[view](#${link})`; - } - - if (object.oneOf) { - console.error('Found oneOf default:', object.oneOf); - } - - return nullMarkdown; - } - - function refLinkConstructor(string, nullable = false) { - const name = string.replace('#/definitions/', ''); - return `[\`${name}\`](#${name.toLowerCase()})${nullable ? '?' : ''}`; - } - - const config = buildObject(null, schema, 1).join('\n'); - fs.writeFileSync(targetPath, `${header}${config}`); - } else { - console.log( - 'Tauri submodule is not initialized, respective core config routes will not be rendered.' - ); - } -}