From a7eafb720a1b547e6b176a125f66984598e74241 Mon Sep 17 00:00:00 2001 From: 6r1d Date: Wed, 14 Sep 2022 17:56:13 +0300 Subject: [PATCH] [docs] #89: Add scripts, Vue component, parser extension, update tutorial Co-authored-by: 0x009922 Co-authored-by: William Richter <88400283+WRRicht3r@users.noreply.github.com> Signed-off-by: 6r1d --- .gitignore | 1 + .vitepress/config.ts | 25 +- .vitepress/snippet_tabs.ts | 417 ++++++++++++++ .vitepress/theme/components/SnippetTabs.vue | 244 ++++++++ .vitepress/theme/icons/Bash.svg | 1 + .vitepress/theme/icons/Java.svg | 1 + .vitepress/theme/icons/JavaScript.svg | 1 + .vitepress/theme/icons/Python.svg | 1 + .vitepress/theme/icons/Rust.svg | 1 + .vitepress/theme/icons/TypeScript.svg | 1 + .vitepress/theme/index.ts | 9 +- package.json | 12 +- pnpm-lock.yaml | 597 +++++++++++++++++++- scripts/constants.mts | 40 ++ scripts/file_utils.mts | 93 +++ scripts/get_snippets.mts | 242 ++++++++ scripts/get_snippets_runner.mts | 8 + scripts/pre.mts | 87 +++ scripts/pre_runner.mts | 8 + scripts/types.mts | 42 ++ scripts/util.mts | 94 +++ snippet_sources.json | 32 ++ src/documenting/snippets.md | 277 +++++++++ src/example_code/ILorem.java | 7 + src/example_code/lorem.js | 3 + src/example_code/lorem.py | 3 + src/example_code/lorem.rs | 5 + src/example_code/lorem.sh | 4 + src/example_code/lorem.ts | 3 + 29 files changed, 2230 insertions(+), 29 deletions(-) create mode 100644 .vitepress/snippet_tabs.ts create mode 100644 .vitepress/theme/components/SnippetTabs.vue create mode 100644 .vitepress/theme/icons/Bash.svg create mode 100644 .vitepress/theme/icons/Java.svg create mode 100644 .vitepress/theme/icons/JavaScript.svg create mode 100644 .vitepress/theme/icons/Python.svg create mode 100644 .vitepress/theme/icons/Rust.svg create mode 100644 .vitepress/theme/icons/TypeScript.svg create mode 100644 scripts/constants.mts create mode 100644 scripts/file_utils.mts create mode 100644 scripts/get_snippets.mts create mode 100755 scripts/get_snippets_runner.mts create mode 100644 scripts/pre.mts create mode 100755 scripts/pre_runner.mts create mode 100644 scripts/types.mts create mode 100644 scripts/util.mts create mode 100644 snippet_sources.json create mode 100644 src/documenting/snippets.md create mode 100644 src/example_code/ILorem.java create mode 100644 src/example_code/lorem.js create mode 100644 src/example_code/lorem.py create mode 100644 src/example_code/lorem.rs create mode 100644 src/example_code/lorem.sh create mode 100644 src/example_code/lorem.ts diff --git a/.gitignore b/.gitignore index a731a9efb..dfeb68bb6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist /src/flymd.md /src/flymd.html /src/*.temp +/src/snippets diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 2990a5d86..913477122 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -2,8 +2,11 @@ import { defineConfig, UserConfig, DefaultTheme } from 'vitepress' import Windi from 'vite-plugin-windicss' import footnote from 'markdown-it-footnote' import customHighlight from './plugins/highlight' -import path from 'path' +import { resolve } from 'path' import { VitePWA } from 'vite-plugin-pwa' +import { snippets_plugin } from './snippet_tabs' +import svgLoader from 'vite-svg-loader' +import { getHighlighter } from "shiki"; async function themeConfig() { const cfg: UserConfig = { @@ -157,6 +160,15 @@ function getGuideSidebar(): DefaultTheme.SidebarGroup[] { }, ], }, + { + text: 'Documenting Iroha', + items: [ + { + text: 'Code snippets', + link: '/documenting/snippets', + }, + ], + }, ] } @@ -172,7 +184,7 @@ export default defineConfig({ lang: 'en-US', vite: { plugins: [ - Windi({ config: path.resolve(__dirname, '../windi.config.ts') }), + Windi({ config: resolve(__dirname, '../windi.config.ts') }), VitePWA({ // Based on: https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs manifest: { @@ -193,6 +205,7 @@ export default defineConfig({ strategies: 'injectManifest', injectRegister: false, }), + svgLoader() ], }, lastUpdated: true, @@ -205,8 +218,14 @@ export default defineConfig({ ], markdown: { - config(md) { + async config(md) { md.use(footnote) + snippets_plugin(md, { + 'snippet_root': resolve(__dirname, '../src/snippets/'), + 'highlighter': await getHighlighter({ + theme: "github-light" + }) + }) }, }, diff --git a/.vitepress/snippet_tabs.ts b/.vitepress/snippet_tabs.ts new file mode 100644 index 000000000..42e4c774a --- /dev/null +++ b/.vitepress/snippet_tabs.ts @@ -0,0 +1,417 @@ +"use strict"; + +import { readFileSync } from "fs"; +import { resolve, join } from "path"; +import { renderToHtml } from "shiki"; + +/** + * Load JSON or return an Error instance + * + * @param {string} jsonPath - a path to the JSON file + * @returns {any} The deserialized file contents + */ +function loadJsonFile(jsonPath: string): any { + // Start with an exception by default + let result = new Error("No sources available"); + // Load a file + try { + // Load sources + let exampleSourcesStr = readFileSync(resolve(jsonPath), "utf8"); + // Parse the result + if (exampleSourcesStr) { + result = JSON.parse(exampleSourcesStr); + } + } catch (error) { + result = error; + } + // Return the sources or an Error instance + return result; +} + +/** + * Generates a Shiki HTML output without the wrapping tags. + * + * @param {string} code - the code to highlight + * @param {string} highlightLang - highlight language + * @param {any} highlighter - Shiki highlighter instance + * @returns {any} The deserialized file contents + */ +const highlightCode = ( + code: string, + highlightLang: string, + highlighter: any +): string => { + const tokens = highlighter.codeToThemedTokens(code, highlightLang); + const html: string = renderToHtml(tokens, { + fg: highlighter.getForegroundColor("github-light"), + elements: { + pre({ _, __, children }) { + return `${children}`; + }, + code({ _, __, children }) { + return `${children}`; + } + } + }); + return html; +}; + +// Vue component tag to be used +const COMP_TAG = "SnippetTabs"; + +// A custom error type used for +// snippet or snippet metadata access errors +class SnippetAccessError extends Error { + constructor(message: string) { + super(message); + this.name = "SnippetAccessError"; + } +} + +// A level change marker for markdown-it +enum TokenNesting { + Opening = -1, + SelfClosing = 0, + Closing = 1 +} + +/** + * Redefine the Token type from markdown-it to avoid importing CJS + * https://markdown-it.github.io/markdown-it/#Token + * + * @typedef {object} Token + */ +type Token = { + // Source map info. Format: [ line_begin, line_end ]. + map: number[]; + // Used in the renderer to calculate line breaks. + // True for block-level tokens. + // False for inline tokens. + block: boolean; + // '*' or '_' for emphasis, fence string for fence, etc. + markup: string; + // Info string for "fence" tokens + info: string; + // Level change marker + nesting: TokenNesting; +}; + +/** + * Redefine the StateBlock type from markdown-it that represents a parser state + * to avoid importing CJS + * + * @typedef {object} StateBlock + */ +type StateBlock = { + line: number; + push(arg0: string, arg1: string, arg2: number): Token; + skipSpaces(pos: number): number; + src: string; + bMarks: number[]; + eMarks: number[]; + tShift: number[]; + sCount: number[]; + lineMax: number; + parentType: string; + blkIndent: number; +}; + +/** + * Redefine a type that (vaguely) represents markdown-it + * + * @typedef {object} MarkdownIt + */ +type MarkdownIt = { + block: any; + renderer: any; +}; + +/** + * A function that splits string by new lines + * and trims each of those lines. + * + * @param {string} input - an input string + * @returns {string[]} a list of trimmed lines + */ +function splitAndTrimLines(input: string): Array { + return input.split(/\r?\n/).map((item) => item.trim()); +} + +/** + * A function that composes a new string out of file names + * provided to it. + * + * Used by the "snippetLocRender" function to render + * the contents of a Vue component slot. + * This component will then display the snippets. + * + * @param {string[]} filenames - a list of file path strings + * @param {string} snippetRoot - a root directory for the snippets + * @returns {string} a string, composed of file contents + */ +function fileListToHighlightedStr( + filenames: string[], + snippetRoot: string, + metaPath: string, + highlighter: any +): string { + let result = ""; + let snippetMeta: { [x: string]: any } = loadJsonFile(metaPath) + if (snippetMeta instanceof Error) { + const mdMsg = + `Unable to read a metadata file.\n` + + `Filename: metadata.json".\n` + + `Directory path: "${snippetRoot}".\n\n` + + `Regenerate snippets if it doesn't exist, fix access rights otherwise.\n` + + `To regenerate the snippets, run "npm run get_snippets" ` + + `or "pnpm run get_snippets" command.\n` + + `Read more in "Documenting Iroha" → "Code snippets" part of the tutorial.` + + `\n`; + throw new SnippetAccessError(mdMsg); + } + for (let filenameId = 0; filenameId < filenames.length; filenameId++) { + const lineFilename = filenames[filenameId].trim(); + const linePath = join(snippetRoot, lineFilename); + const lineMeta = snippetMeta[lineFilename]; + try { + let fileContent = readFileSync(linePath).toString(); + let highlightedCode = highlightCode( + fileContent, + lineMeta["lang"], + highlighter + ); + let tabHtml = + `
` +
+        `${highlightedCode}
`; + result += tabHtml; + } catch (err) { + const msg = + `Unable to read a file.\n` + + `Filename: "${lineFilename}".\n` + + `Directory path: "${snippetRoot}".\n\n` + + `Ensure it exists, its location is correct and its access rights allow to read it.\n` + + `If you did not download the snippets, use the "npm run get_snippets" ` + + `or "pnpm run get_snippets" command.\n` + + `Read more in "Documenting Iroha" → "Code snippets" part of the tutorial.` + + `\n`; + throw new SnippetAccessError(msg); + } + } + return result; +} + +/** + * A function that initializes a snippet group markdown-it plugin. + */ +export function snippets_plugin(md: MarkdownIt, options: Record) { + /** + * A function that validates snippet parameters and allows it to be rendered. + * If a path is incorrect, rendering won't happen. + * + * @param {string} params - a parameter string that points out a path to the snippets + * @returns {bool} - whether the snippet directory exists or not + */ + function validateDefault(params: string): boolean { + return params.toLowerCase() == 'snippets'; + } + + /** + * Render a section with a pre-defined wrapper tag + * + * @param {string} tokens - a list of markdown-it token instances + */ + function snippetRender(tokens: Array, idx: number): string { + if (tokens[idx].nesting === 1) { + // Render an opening tag + return `<${COMP_TAG}>\n`; + } else { + // Render an closing tag + return `\n`; + } + } + + /** + * Render slots inside the SnippetTabs Vue component. + * + * Locates the internal path or an updated one, + * outputs the contents of files inside. + * + * @param {Array} tokens - array of Markdown token instances + * @param {number} idx + * @returns {string} - render results + */ + function snippetLocRender(tokens: Array, idx: number): string { + const pathStr = + options.snippet_root || + join(options.snippet_root, tokens[idx - 1].info.trim()); + const snippetRoot: string = resolve(pathStr); + const metaPath: string = join(snippetRoot, 'meta.json'); + const filenames: string[] = splitAndTrimLines(tokens[idx].info.trim()); + return `${fileListToHighlightedStr( + filenames, + snippetRoot, + metaPath, + options.highlighter + )}\n`; + } + + options = options || {}; + + let min_markers: number = 2, + marker_str: string = ":", + marker_char: number = marker_str.charCodeAt(0), + marker_len: number = marker_str.length, + validate: Function = options.validate || validateDefault, + render: Function = snippetRender; + + if ( + !options.hasOwnProperty("snippet_root") || + options.snippet_root.constructor.name !== "String" + ) { + const errTxt = + "Incorrect configuration. " + + "A correct value for snippet_root is required for snippet_tabs plugin."; + throw new Error(errTxt); + } + + function snippet_container( + state: StateBlock, + startLine: number, + endLine: number, + silent: boolean + ) { + let pos: number, + nextLine: number, + marker_count: number, + markup: string, + params: string, + token: Token, + old_parent: string, + old_line_max: number, + auto_closed = false, + start: number = state.bMarks[startLine] + state.tShift[startLine], + max: number = state.eMarks[startLine]; + + // Check out the first character quickly + // to filter out most of non-containers + if (marker_char !== state.src.charCodeAt(start)) { + return false; + } + + // Continue checking of the marker string + for (pos = start + 1; pos <= max; pos++) { + if (marker_str[(pos - start) % marker_len] !== state.src[pos]) { + break; + } + } + + marker_count = Math.floor((pos - start) / marker_len); + if (marker_count < min_markers) { + return false; + } + pos -= (pos - start) % marker_len; + + markup = state.src.slice(start, pos); + params = state.src.slice(pos, max); + // Ignore a string that does not get validated + if (!validate(params, markup)) { + return false; + } + + // Since start is found, we can report success here in validation mode + if (silent) return true; + + // Search for the end of the block + nextLine = startLine; + + for (;;) { + nextLine++; + if (nextLine >= endLine) { + // Non-closed block should be autoclosed by end of document. + // Also, block seems to be + // automatically closed by the end of a parent one. + break; + } + + start = state.bMarks[nextLine] + state.tShift[nextLine]; + max = state.eMarks[nextLine]; + + if (start < max && state.sCount[nextLine] < state.blkIndent) { + // non-empty line with negative indent should stop the list: + // - ``` + // test + break; + } + + if (marker_char !== state.src.charCodeAt(start)) { + continue; + } + + if (state.sCount[nextLine] - state.blkIndent >= 4) { + // closing fence should be indented less than 4 spaces + continue; + } + + for (pos = start + 1; pos <= max; pos++) { + if (marker_str[(pos - start) % marker_len] !== state.src[pos]) { + break; + } + } + + // closing code fence must be at least as long as the opening one + if (Math.floor((pos - start) / marker_len) < marker_count) { + continue; + } + + // make sure tail has spaces only + pos -= (pos - start) % marker_len; + pos = state.skipSpaces(pos); + + if (pos < max) { + continue; + } + + // found! + auto_closed = true; + break; + } + + old_parent = state.parentType; + old_line_max = state.lineMax; + state.parentType = "snippets"; + + // Prevent the lazy continuations from ever going past an end marker + state.lineMax = nextLine; + + token = state.push("snippets_open", "div", 1); + token.markup = markup; + token.block = true; + token.info = params; + token.map = [startLine, nextLine]; + + token = state.push("snippet_locations", "div", 1); + token.markup = markup; + token.block = true; + token.info = state.src.slice( + state.bMarks[startLine + 1], + state.bMarks[nextLine] + ); + token.map = [startLine, nextLine]; + + token = state.push("snippets_close", "div", -1); + token.markup = state.src.slice(start, pos); + token.block = true; + + state.parentType = old_parent; + state.lineMax = old_line_max; + state.line = nextLine + (auto_closed ? 1 : 0); + + return true; + } + + md.block.ruler.before("fence", "snippets", snippet_container, {}); + md.renderer.rules["snippets_open"] = render; + md.renderer.rules["snippets_close"] = render; + md.renderer.rules["snippet_locations"] = snippetLocRender; +} diff --git a/.vitepress/theme/components/SnippetTabs.vue b/.vitepress/theme/components/SnippetTabs.vue new file mode 100644 index 000000000..aeb5ed8ae --- /dev/null +++ b/.vitepress/theme/components/SnippetTabs.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/.vitepress/theme/icons/Bash.svg b/.vitepress/theme/icons/Bash.svg new file mode 100644 index 000000000..a63895427 --- /dev/null +++ b/.vitepress/theme/icons/Bash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.vitepress/theme/icons/Java.svg b/.vitepress/theme/icons/Java.svg new file mode 100644 index 000000000..a055158ea --- /dev/null +++ b/.vitepress/theme/icons/Java.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.vitepress/theme/icons/JavaScript.svg b/.vitepress/theme/icons/JavaScript.svg new file mode 100644 index 000000000..0b91a171a --- /dev/null +++ b/.vitepress/theme/icons/JavaScript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.vitepress/theme/icons/Python.svg b/.vitepress/theme/icons/Python.svg new file mode 100644 index 000000000..ac20b6490 --- /dev/null +++ b/.vitepress/theme/icons/Python.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.vitepress/theme/icons/Rust.svg b/.vitepress/theme/icons/Rust.svg new file mode 100644 index 000000000..b62a248ed --- /dev/null +++ b/.vitepress/theme/icons/Rust.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.vitepress/theme/icons/TypeScript.svg b/.vitepress/theme/icons/TypeScript.svg new file mode 100644 index 000000000..f60f32ca8 --- /dev/null +++ b/.vitepress/theme/icons/TypeScript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index 6f671ebeb..33b19f7f0 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -1,7 +1,12 @@ import ThemeDefault from 'vitepress/theme' -// import Layout from './components/Layout.vue' +import SnippetTabs from './components/SnippetTabs.vue' import 'virtual:windi.css' import './style/index.scss' -export default ThemeDefault +export default { + ...ThemeDefault, + enhanceApp({ app }) { + app.component('SnippetTabs', SnippetTabs); + } +} diff --git a/package.json b/package.json index 025d5570c..789f9b670 100644 --- a/package.json +++ b/package.json @@ -7,22 +7,30 @@ "serve": "vitepress serve", "format:docs": "prettier 'src/**/*.md'", "format:docs:check": "pnpm run format:docs --check", - "format:docs:fix": "pnpm run format:docs --write" + "format:docs:fix": "pnpm run format:docs --write", + "get_snippets": "tsx scripts/get_snippets_runner.mts", + "postinstall": "tsx scripts/pre_runner.mts" }, "devDependencies": { "@types/node": "^18.0.3", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", + "axios": "^0.27.2", + "ci-info": "^3.4.0", + "dst-parser": "^0.0.11", "eslint": "^8.19.0", "eslint-plugin-vue": "^9.2.0", "markdown-it-footnote": "^3.0.3", + "ora": "^6.1.2", "prettier": "^2.7.1", "prettier-eslint": "^15.0.1", "sass": "^1.53.0", - "shiki": "^0.10.1", + "shiki": "^0.11.1", + "tsx": "^3.9.0", "typescript": "^4.7.4", "vite-plugin-pwa": "^0.12.3", "vite-plugin-windicss": "^1.8.6", + "vite-svg-loader": "^3.6.0", "vitepress": "1.0.0-alpha.13", "windicss": "^3.5.6" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5c9c5400..38f7375aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,16 +5,22 @@ specifiers: '@typescript-eslint/eslint-plugin': ^5.30.5 '@typescript-eslint/parser': ^5.30.5 '@vueuse/core': ^8.9.1 + axios: ^0.27.2 + ci-info: ^3.4.0 + dst-parser: ^0.0.11 eslint: ^8.19.0 eslint-plugin-vue: ^9.2.0 markdown-it-footnote: ^3.0.3 + ora: ^6.1.2 prettier: ^2.7.1 prettier-eslint: ^15.0.1 sass: ^1.53.0 - shiki: ^0.10.1 + shiki: ^0.11.1 + tsx: ^3.9.0 typescript: ^4.7.4 vite-plugin-pwa: ^0.12.3 vite-plugin-windicss: ^1.8.6 + vite-svg-loader: ^3.6.0 vitepress: 1.0.0-alpha.13 vue: ^3.2.37 windicss: ^3.5.6 @@ -27,16 +33,22 @@ devDependencies: '@types/node': 18.0.3 '@typescript-eslint/eslint-plugin': 5.30.5_6zdoc3rn4mpiddqwhppni2mnnm '@typescript-eslint/parser': 5.30.5_4x5o4skxv6sl53vpwefgt23khm + axios: 0.27.2 + ci-info: 3.4.0 + dst-parser: 0.0.11 eslint: 8.19.0 eslint-plugin-vue: 9.2.0_eslint@8.19.0 markdown-it-footnote: 3.0.3 + ora: 6.1.2 prettier: 2.7.1 prettier-eslint: 15.0.1 sass: 1.53.0 - shiki: 0.10.1 + shiki: 0.11.1 + tsx: 3.9.0 typescript: 4.7.4 vite-plugin-pwa: 0.12.3 vite-plugin-windicss: 1.8.6 + vite-svg-loader: 3.6.0 vitepress: 1.0.0-alpha.13_sass@1.53.0 windicss: 3.5.6 @@ -1365,6 +1377,27 @@ packages: - '@algolia/client-search' dev: true + /@esbuild-kit/cjs-loader/2.3.3: + resolution: {integrity: sha512-Rt4O1mXlPEDVxvjsHLgbtHVdUXYK9C1/6ThpQnt7FaXIjUOsI6qhHYMgALhNnlIMZffag44lXd6Dqgx3xALbpQ==} + dependencies: + '@esbuild-kit/core-utils': 2.3.0 + get-tsconfig: 4.2.0 + dev: true + + /@esbuild-kit/core-utils/2.3.0: + resolution: {integrity: sha512-JL73zt/LN/qqziHuod4/bM2xBNNofDZu1cbwT6KIn6B11lA4cgDXkoSHOfNCbZMZOnh0Aqf0vW/gNQC+Z18hKQ==} + dependencies: + esbuild: 0.15.7 + source-map-support: 0.5.21 + dev: true + + /@esbuild-kit/esm-loader/2.4.2: + resolution: {integrity: sha512-N9dPKAj8WOx6djVnStgILWXip4fjDcBk9L7azO0/uQDpu8Ee0eaL78mkN4Acid9BzvNAKWwdYXFJZnsVahNEew==} + dependencies: + '@esbuild-kit/core-utils': 2.3.0 + get-tsconfig: 4.2.0 + dev: true + /@esbuild/linux-loong64/0.14.54: resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} engines: {node: '>=12'} @@ -1374,6 +1407,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64/0.15.7: + resolution: {integrity: sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@eslint/eslintrc/1.3.0: resolution: {integrity: sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1535,6 +1577,11 @@ packages: string.prototype.matchall: 4.0.7 dev: true + /@trysound/sax/0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: true + /@types/eslint/8.4.3: resolution: {integrity: sha512-YP1S7YJRMPs+7KZKDb9G63n8YejIwW9BALq7a5j2+H4yl6iOv9CB29edho+cuFRrvmJbbaH2yiVChKLJVysDGw==} dependencies: @@ -1946,6 +1993,11 @@ packages: engines: {node: '>=8'} dev: true + /ansi-regex/6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + /ansi-styles/2.2.1: resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} engines: {node: '>=0.10.0'} @@ -1986,11 +2038,24 @@ packages: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} dev: true + /asynckit/0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true + /at-least-node/1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} dev: true + /axios/0.27.2: + resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + transitivePeerDependencies: + - debug + dev: true + /babel-plugin-dynamic-import-node/2.3.3: resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} dependencies: @@ -2037,11 +2102,23 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /base64-js/1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + /binary-extensions/2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} dev: true + /bl/5.0.0: + resolution: {integrity: sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==} + dependencies: + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: true + /body-scroll-lock/4.0.0-beta.0: resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==} dev: true @@ -2085,6 +2162,13 @@ packages: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true + /buffer/6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + /builtin-modules/3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} @@ -2134,6 +2218,11 @@ packages: supports-color: 7.2.0 dev: true + /chalk/5.0.1: + resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: true + /chokidar/3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -2149,6 +2238,27 @@ packages: fsevents: 2.3.2 dev: true + /ci-info/3.4.0: + resolution: {integrity: sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug==} + dev: true + + /cli-cursor/4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + restore-cursor: 4.0.0 + dev: true + + /cli-spinners/2.7.0: + resolution: {integrity: sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==} + engines: {node: '>=6'} + dev: true + + /clone/1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + dev: true + /color-convert/1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -2170,10 +2280,22 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /combined-stream/1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + /commander/2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: true + /commander/7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: true + /common-tags/1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} engines: {node: '>=4.0.0'} @@ -2210,12 +2332,42 @@ packages: engines: {node: '>=8'} dev: true + /css-select/4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + dev: true + + /css-tree/1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + dev: true + + /css-what/6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: true + /cssesc/3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true dev: true + /csso/4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + dependencies: + css-tree: 1.1.3 + dev: true + /csstype/2.6.20: resolution: {integrity: sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==} @@ -2240,6 +2392,12 @@ packages: engines: {node: '>=0.10.0'} dev: true + /defaults/1.0.3: + resolution: {integrity: sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==} + dependencies: + clone: 1.0.4 + dev: true + /define-properties/1.1.4: resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} engines: {node: '>= 0.4'} @@ -2248,6 +2406,11 @@ packages: object-keys: 1.1.1 dev: true + /delayed-stream/1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: true + /dir-glob/3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2266,6 +2429,38 @@ packages: esutils: 2.0.3 dev: true + /dom-serializer/1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + dev: true + + /domelementtype/2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domhandler/4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils/2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + dev: true + + /dst-parser/0.0.11: + resolution: {integrity: sha512-1p29Pm05Q8cD0wjzIqekz7tnrkT4UEoKF3l5jRvGgTCg5RaOOTVFbZtUO43ywjWqTdAPBZteIAjGFGPPs/LeIA==} + requiresBuild: true + dev: true + /ejs/3.1.8: resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} engines: {node: '>=0.10.0'} @@ -2278,6 +2473,10 @@ packages: resolution: {integrity: sha512-h+Fadt1gIaQ06JaIiyqPsBjJ08fV5Q7md+V8bUvQW/9OvXfL2LRICTz2EcnnCP7QzrFTS6/27MRV6Bl9Yn97zA==} dev: true + /entities/2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + dev: true + /es-abstract/1.20.1: resolution: {integrity: sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==} engines: {node: '>= 0.4'} @@ -2325,6 +2524,15 @@ packages: dev: true optional: true + /esbuild-android-64/0.15.7: + resolution: {integrity: sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /esbuild-android-arm64/0.14.54: resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} engines: {node: '>=12'} @@ -2334,6 +2542,15 @@ packages: dev: true optional: true + /esbuild-android-arm64/0.15.7: + resolution: {integrity: sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /esbuild-darwin-64/0.14.54: resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} engines: {node: '>=12'} @@ -2343,6 +2560,15 @@ packages: dev: true optional: true + /esbuild-darwin-64/0.15.7: + resolution: {integrity: sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /esbuild-darwin-arm64/0.14.54: resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} engines: {node: '>=12'} @@ -2352,6 +2578,15 @@ packages: dev: true optional: true + /esbuild-darwin-arm64/0.15.7: + resolution: {integrity: sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /esbuild-freebsd-64/0.14.54: resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} engines: {node: '>=12'} @@ -2361,6 +2596,15 @@ packages: dev: true optional: true + /esbuild-freebsd-64/0.15.7: + resolution: {integrity: sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /esbuild-freebsd-arm64/0.14.54: resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} engines: {node: '>=12'} @@ -2370,6 +2614,15 @@ packages: dev: true optional: true + /esbuild-freebsd-arm64/0.15.7: + resolution: {integrity: sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /esbuild-linux-32/0.14.54: resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} engines: {node: '>=12'} @@ -2379,6 +2632,15 @@ packages: dev: true optional: true + /esbuild-linux-32/0.15.7: + resolution: {integrity: sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /esbuild-linux-64/0.14.54: resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} engines: {node: '>=12'} @@ -2388,6 +2650,15 @@ packages: dev: true optional: true + /esbuild-linux-64/0.15.7: + resolution: {integrity: sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /esbuild-linux-arm/0.14.54: resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} engines: {node: '>=12'} @@ -2397,6 +2668,15 @@ packages: dev: true optional: true + /esbuild-linux-arm/0.15.7: + resolution: {integrity: sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /esbuild-linux-arm64/0.14.54: resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} engines: {node: '>=12'} @@ -2406,6 +2686,15 @@ packages: dev: true optional: true + /esbuild-linux-arm64/0.15.7: + resolution: {integrity: sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /esbuild-linux-mips64le/0.14.54: resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} engines: {node: '>=12'} @@ -2415,6 +2704,15 @@ packages: dev: true optional: true + /esbuild-linux-mips64le/0.15.7: + resolution: {integrity: sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /esbuild-linux-ppc64le/0.14.54: resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} engines: {node: '>=12'} @@ -2424,6 +2722,15 @@ packages: dev: true optional: true + /esbuild-linux-ppc64le/0.15.7: + resolution: {integrity: sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /esbuild-linux-riscv64/0.14.54: resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} engines: {node: '>=12'} @@ -2433,6 +2740,15 @@ packages: dev: true optional: true + /esbuild-linux-riscv64/0.15.7: + resolution: {integrity: sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /esbuild-linux-s390x/0.14.54: resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} engines: {node: '>=12'} @@ -2442,6 +2758,15 @@ packages: dev: true optional: true + /esbuild-linux-s390x/0.15.7: + resolution: {integrity: sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /esbuild-netbsd-64/0.14.54: resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} engines: {node: '>=12'} @@ -2451,6 +2776,15 @@ packages: dev: true optional: true + /esbuild-netbsd-64/0.15.7: + resolution: {integrity: sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /esbuild-openbsd-64/0.14.54: resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} engines: {node: '>=12'} @@ -2460,6 +2794,15 @@ packages: dev: true optional: true + /esbuild-openbsd-64/0.15.7: + resolution: {integrity: sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /esbuild-sunos-64/0.14.54: resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} engines: {node: '>=12'} @@ -2469,6 +2812,15 @@ packages: dev: true optional: true + /esbuild-sunos-64/0.15.7: + resolution: {integrity: sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /esbuild-windows-32/0.14.54: resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} engines: {node: '>=12'} @@ -2478,6 +2830,15 @@ packages: dev: true optional: true + /esbuild-windows-32/0.15.7: + resolution: {integrity: sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /esbuild-windows-64/0.14.54: resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} engines: {node: '>=12'} @@ -2487,6 +2848,15 @@ packages: dev: true optional: true + /esbuild-windows-64/0.15.7: + resolution: {integrity: sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /esbuild-windows-arm64/0.14.54: resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} engines: {node: '>=12'} @@ -2496,6 +2866,15 @@ packages: dev: true optional: true + /esbuild-windows-arm64/0.15.7: + resolution: {integrity: sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /esbuild/0.14.54: resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} engines: {node: '>=12'} @@ -2525,6 +2904,35 @@ packages: esbuild-windows-arm64: 0.14.54 dev: true + /esbuild/0.15.7: + resolution: {integrity: sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/linux-loong64': 0.15.7 + esbuild-android-64: 0.15.7 + esbuild-android-arm64: 0.15.7 + esbuild-darwin-64: 0.15.7 + esbuild-darwin-arm64: 0.15.7 + esbuild-freebsd-64: 0.15.7 + esbuild-freebsd-arm64: 0.15.7 + esbuild-linux-32: 0.15.7 + esbuild-linux-64: 0.15.7 + esbuild-linux-arm: 0.15.7 + esbuild-linux-arm64: 0.15.7 + esbuild-linux-mips64le: 0.15.7 + esbuild-linux-ppc64le: 0.15.7 + esbuild-linux-riscv64: 0.15.7 + esbuild-linux-s390x: 0.15.7 + esbuild-netbsd-64: 0.15.7 + esbuild-openbsd-64: 0.15.7 + esbuild-sunos-64: 0.15.7 + esbuild-windows-32: 0.15.7 + esbuild-windows-64: 0.15.7 + esbuild-windows-arm64: 0.15.7 + dev: true + /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -2744,6 +3152,25 @@ packages: resolution: {integrity: sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==} dev: true + /follow-redirects/1.15.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: true + + /form-data/4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + /fs-extra/9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -2813,6 +3240,10 @@ packages: get-intrinsic: 1.1.2 dev: true + /get-tsconfig/4.2.0: + resolution: {integrity: sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==} + dev: true + /glob-parent/5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2916,6 +3347,10 @@ packages: resolution: {integrity: sha512-jjKrT1EnyZewQ/gCBb/eyiYrhGzws2FeY92Yx8qT9S9GeQAmo4JFVIiWRIfKW/6Ob9A+UDAOW9j9jn58fy2HIg==} dev: true + /ieee754/1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + /ignore/5.2.0: resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} engines: {node: '>= 4'} @@ -3014,6 +3449,11 @@ packages: is-extglob: 2.1.1 dev: true + /is-interactive/2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + dev: true + /is-module/1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} dev: true @@ -3078,6 +3518,11 @@ packages: has-symbols: 1.0.3 dev: true + /is-unicode-supported/1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: true + /is-weakref/1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: @@ -3157,10 +3602,6 @@ packages: hasBin: true dev: true - /jsonc-parser/3.0.0: - resolution: {integrity: sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==} - dev: true - /jsonc-parser/3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} dev: true @@ -3211,6 +3652,14 @@ packages: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: true + /log-symbols/5.1.0: + resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} + engines: {node: '>=12'} + dependencies: + chalk: 5.0.1 + is-unicode-supported: 1.3.0 + dev: true + /loglevel-colored-level-prefix/1.0.0: resolution: {integrity: sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==} dependencies: @@ -3246,6 +3695,10 @@ packages: resolution: {integrity: sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w==} dev: true + /mdn-data/2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + dev: true + /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -3263,6 +3716,23 @@ packages: picomatch: 2.3.1 dev: true + /mime-db/1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types/2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /mimic-fn/2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -3329,6 +3799,13 @@ packages: wrappy: 1.0.2 dev: true + /onetime/5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + /optionator/0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} engines: {node: '>= 0.8.0'} @@ -3341,6 +3818,21 @@ packages: word-wrap: 1.2.3 dev: true + /ora/6.1.2: + resolution: {integrity: sha512-EJQ3NiP5Xo94wJXIzAyOtSb0QEIAUu7m8t6UZ9krbz0vAJqr92JpcK/lEXg91q6B9pEGqrykkd2EQplnifDSBw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + bl: 5.0.0 + chalk: 5.0.1 + cli-cursor: 4.0.0 + cli-spinners: 2.7.0 + is-interactive: 2.0.0 + is-unicode-supported: 1.3.0 + log-symbols: 5.1.0 + strip-ansi: 7.0.1 + wcwidth: 1.0.1 + dev: true + /parent-module/1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3469,6 +3961,15 @@ packages: safe-buffer: 5.2.1 dev: true + /readable-stream/3.6.0: + resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + /readdirp/3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -3557,6 +4058,14 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /restore-cursor/4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + /reusify/1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3657,14 +4166,6 @@ packages: engines: {node: '>=8'} dev: true - /shiki/0.10.1: - resolution: {integrity: sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==} - dependencies: - jsonc-parser: 3.0.0 - vscode-oniguruma: 1.6.1 - vscode-textmate: 5.2.0 - dev: true - /shiki/0.11.1: resolution: {integrity: sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==} dependencies: @@ -3681,6 +4182,10 @@ packages: object-inspect: 1.12.2 dev: true + /signal-exit/3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3711,6 +4216,11 @@ packages: /sourcemap-codec/1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + /stable/0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + dev: true + /string.prototype.matchall/4.0.7: resolution: {integrity: sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==} dependencies: @@ -3740,6 +4250,12 @@ packages: es-abstract: 1.20.1 dev: true + /string_decoder/1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + /stringify-object/3.3.0: resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} engines: {node: '>=4'} @@ -3763,6 +4279,13 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-ansi/7.0.1: + resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + /strip-comments/2.0.1: resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} engines: {node: '>=10'} @@ -3797,6 +4320,20 @@ packages: engines: {node: '>= 0.4'} dev: true + /svgo/2.8.0: + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} + engines: {node: '>=10.13.0'} + hasBin: true + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 4.3.0 + css-tree: 1.1.3 + csso: 4.2.0 + picocolors: 1.0.0 + stable: 0.1.8 + dev: true + /temp-dir/2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} @@ -3858,6 +4395,17 @@ packages: typescript: 4.7.4 dev: true + /tsx/3.9.0: + resolution: {integrity: sha512-ofxsE+qjqCYYq4UBt5khglvb+ESgxef1YpuNcdQI92kvcAT2tZVrnSK3g4bRXTUhLmKHcC5q8vIZA47os/stng==} + hasBin: true + dependencies: + '@esbuild-kit/cjs-loader': 2.3.3 + '@esbuild-kit/core-utils': 2.3.0 + '@esbuild-kit/esm-loader': 2.4.2 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /type-check/0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3984,6 +4532,13 @@ packages: - supports-color dev: true + /vite-svg-loader/3.6.0: + resolution: {integrity: sha512-bZJffcgCREW57kNkgMhuNqeDznWXyQwJ3wKrRhHLMMzwDnP5jr3vXW3cqsmquRR7VTP5mLdKj1/zzPPooGUuPw==} + dependencies: + '@vue/compiler-sfc': 3.2.37 + svgo: 2.8.0 + dev: true + /vite/3.0.9_sass@1.53.0: resolution: {integrity: sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4037,18 +4592,10 @@ packages: - terser dev: true - /vscode-oniguruma/1.6.1: - resolution: {integrity: sha512-vc4WhSIaVpgJ0jJIejjYxPvURJavX6QG41vu0mGhqywMkQqulezEqEQ3cO3gc8GvcOpX6ycmKGqRoROEMBNXTQ==} - dev: true - /vscode-oniguruma/1.6.2: resolution: {integrity: sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA==} dev: true - /vscode-textmate/5.2.0: - resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==} - dev: true - /vscode-textmate/6.0.0: resolution: {integrity: sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ==} dev: true @@ -4128,6 +4675,12 @@ packages: '@vue/server-renderer': 3.2.37_vue@3.2.37 '@vue/shared': 3.2.37 + /wcwidth/1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + dependencies: + defaults: 1.0.3 + dev: true + /webidl-conversions/4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true diff --git a/scripts/constants.mts b/scripts/constants.mts new file mode 100644 index 000000000..1b4ebd951 --- /dev/null +++ b/scripts/constants.mts @@ -0,0 +1,40 @@ +/** + * @module Constants + * + * Constants, used by the documentation scripts. + */ + +import { dirname, resolve, join } from "path"; +import { fileURLToPath } from "url"; +import { SourceDefinition } from "./types.mjs"; +import _sources from "../snippet_sources.json"; + +// Matches languages to the file extensions +export const langExtensions: Record = { + java: "java", + python: "py", + javascript: "js", + typescript: "ts", + rust: "rs", + shellscript: "sh", + shell: "sh", + bash: "sh", + sh: "sh" +}; + +// Locate the current file +export const __filename = fileURLToPath(import.meta.url); +// Locate a directory, containing this file +export const __dirname = dirname(__filename); + +// Resolve a parent directory path +export const SRC_PATH_RESOLVED = resolve(__dirname, ".."); + +// A relative path to the snippet URL list +const SOURCES: SourceDefinition[] = _sources.map((source) => { + return { ...source, content: undefined }; +}); +export { SOURCES }; + +// Resolve a path to the directory needed for saving the snippets +export const SNIPPET_SRC_DIR = resolve(__dirname, "../src/snippets"); diff --git a/scripts/file_utils.mts b/scripts/file_utils.mts new file mode 100644 index 000000000..98b3b81a9 --- /dev/null +++ b/scripts/file_utils.mts @@ -0,0 +1,93 @@ +/** + * @module File_utilities + * + * This file contains the filesystem-related utilities, needed + * by the other scripts. + */ + +import { resolve } from "path"; +import { + readFileSync, + writeFileSync, + PathOrFileDescriptor, + PathLike, + existsSync, + mkdirSync, + readdirSync +} from "fs"; + +/** + * Write a string to a file path synchronously + * + * @export + * @param {string} snippetStr + * @param {PathOrFileDescriptor} filePath + * @returns {(Boolean | Error)} + */ +export function writeStrToFile( + snippetStr: string, + filePath: PathOrFileDescriptor +): Boolean | Error { + let result: Boolean | Error; + try { + writeFileSync(filePath, snippetStr); + result = true; + } catch (err) { + result = err; + } + return result; +} + +/** + * Loads strings from text files. + * Needed for file URL support. + * + * @export + * @param {string} txtPath + * @returns {(string | Error)} file contents or an Error instance + */ +export function pathToStr(txtPath: string): string | Error { + // Start with an exception by default + let result: string | Error = new Error("No sources available"); + // Try to load a file, return an error instance otherwise + try { + const txtPathResolved = resolve(txtPath); + result = readFileSync(txtPathResolved, "utf8"); + } catch (error) { + result = error; + } + // Return the sources or an Error instance + return result; +} + +/** + * Checks if a directory exists and creates a path to it recursively + * if it doesn't. + * + * @export + * @param {PathLike} dir + */ +export function ensureDirExists(dir: PathLike) { + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } +} + +/** + * Checks if a directory contains any files + * + * @export + * @param {PathLike} dirname - path to a directory to check + * @returns {Boolean} - true if there are files in a given directory + */ +export function checkDirHasContents(dirname: PathLike): boolean { + let result: boolean; + let fileList: string[] = []; + try { + fileList = readdirSync(dirname); + result = fileList.length > 0; + } catch (err) { + result = false; + } + return result; +} diff --git a/scripts/get_snippets.mts b/scripts/get_snippets.mts new file mode 100644 index 000000000..567360982 --- /dev/null +++ b/scripts/get_snippets.mts @@ -0,0 +1,242 @@ +/* + * Snippet downloader version utilizing a monad-like pattern. + */ + +import ora from "ora"; +import { join } from "path"; +import { resolve } from "path"; +import { ExampleParser } from "dst-parser"; +import { + SourceDefinition, + IndividualSnippet, + SnippetProcessingState +} from "./types.mjs"; +import { __dirname, SNIPPET_SRC_DIR, SOURCES } from "./constants.mjs"; +import { writeStrToFile, ensureDirExists } from "./file_utils.mjs"; +import { validateSources, collectPage, getSnippetFilename } from "./util.mjs"; + +/** + * Checks the sources for correctness. + * + * @param {SnippetProcessingState} pState + */ +let validateSrcList = async (pState: SnippetProcessingState) => { + const spinner = ora("Validating the sources…").start(); + try { + let sourceValidation = validateSources(SOURCES); + if (sourceValidation !== true) throw sourceValidation; + spinner.succeed("Sources are correct."); + } catch (sve) { + pState.error = sve; + spinner.fail("Sources are empty."); + } +}; + +/** + * Displays the URLs from the source list + * + * @param {SnippetProcessingState} pState + */ +const printSourceList = async (pState: SnippetProcessingState) => { + const spinner = ora("Preparing a source list…").start(); + let msg = "Source list:\n"; + pState.sources.forEach((sourceUrl: SourceDefinition) => { + msg += `* ${sourceUrl["url"]}\n`; + }); + msg = msg.trimEnd(); + spinner.succeed(msg); +}; + +/** + * Loads individual items from the source list + * + * @param {SnippetProcessingState} pState + */ +const collectPagesBeta = async (pState: SnippetProcessingState) => { + if (pState.error === null) { + const spinner = ora("Collecting pages in parallel").start(); + const sourcesNew: (SourceDefinition | Error)[] = await Promise.all( + pState.sources.map((source: SourceDefinition) => + collectPage(source, resolve(__dirname, "..")) + ) + ); + pState.sources = []; + sourcesNew.forEach((source: SourceDefinition | Error) => { + if (source instanceof Error) pState.error = source; + else pState.sources.push(source); + }); + if (pState.error === null) spinner.succeed("Pages were downloaded."); + else spinner.fail("Page downloading failed."); + } +}; + +/** + * Parses the pages with DST-parser + * + * @param {SnippetProcessingState} pState + */ +const parsePagesBeta = async (pState: SnippetProcessingState) => { + pState.parsed = []; + const spinner = ora("Parsing available pages…").start(); + try { + pState.sources.forEach((src: SourceDefinition) => { + // Allow only the SourceDefinition instances with content + if (src.content === undefined) + throw new Error(`No content for ${src.url}`); + // Parse content and fill the list in a "parsed" attribute + const parserInst = new ExampleParser(src.content); + let snippetMap = parserInst.mapLines(); + const tmpItems = Object.entries(snippetMap).map((sn) => { + const snippet: IndividualSnippet = { + name: sn[0], + text: sn[1], + version: src.version || "", + lang: src.lang || "", + url: src.url || "" + }; + return snippet; + }); + pState.parsed.push(...tmpItems); + }); + spinner.succeed("All pages parsed succesfully."); + } catch (parserError) { + pState.error = parserError; + spinner.fail(`Parsing failed: ${parserError.message}`); + } +}; + +/** + * Displays a list of the available snippets + * + * @param {SnippetProcessingState} pState + */ +const printAvailableSnippetsBeta = async (pState: SnippetProcessingState) => { + const spinner = ora("Preparing a snippet list…").start(); + if (pState.parsed) { + let msg = "Snippet list:\n"; + pState.parsed.forEach((snippet: IndividualSnippet) => { + msg += `* [${snippet.lang}] ${snippet.name}\n`; + }); + msg = msg.trimEnd(); + spinner.succeed(msg); + } else { + spinner.succeed("No snippets are currently available."); + } +}; + +/** + * Creates a snippet directory if it doesn't exist + * + * @param {SnippetProcessingState} pState + */ +const ensureSnippetDirBeta = async (pState: SnippetProcessingState) => { + const spinner = ora("Ensuring snippet directory exists…").start(); + try { + ensureDirExists(SNIPPET_SRC_DIR); + pState.output_dir_accessible = true; + spinner.succeed(`Snippet dir: ${SNIPPET_SRC_DIR}`); + } catch (ensureDirError) { + pState.output_dir_accessible = false; + spinner.fail( + `Unable to ensure output dir exists:\n${ensureDirError.message}` + ); + } +}; + +/** + * Sets snippet filenames + * + * @param {SnippetProcessingState} pState + */ +const setSnippetNames = async (pState: SnippetProcessingState) => { + // Snippets to record + pState.output_strings = {}; + try { + // Process snippets in the current group, filling the contents + for (let key in pState.parsed) { + let snippet: IndividualSnippet = pState.parsed[key]; + const snippetFilename = getSnippetFilename(snippet); + pState.output_strings[snippetFilename] = snippet.text; + } + } catch (fmtErr) { + pState.error = fmtErr; + } +}; + +/** + * Export snippet metadata for a tabs component in JSON + * + * @param {SnippetProcessingState} pState + */ +const saveSnippetMeta = async (pState: SnippetProcessingState) => { + const spinner = ora("Saving snippet metadata JSON…").start(); + try { + // Record matches between filenames and the metadata + let outputMeta = {}; + // Process snippets in the current group, filling the contents + for (let key in pState.parsed) { + let rec: IndividualSnippet = pState.parsed[key]; + const snippetFilename = getSnippetFilename(rec); + outputMeta[snippetFilename] = { + version: rec.version, + lang: rec.lang, + name: rec.name + }; + } + writeStrToFile( + JSON.stringify(outputMeta, null, 4), + join(SNIPPET_SRC_DIR, "meta.json") + ); + spinner.succeed("Snippet metadata was saved."); + } catch (snmtErr) { + pState.error = snmtErr; + spinner.fail("Unable to save snippet metadata."); + } +}; + +/** + * Saves each snippet in its own file + * + * @param {SnippetProcessingState} pState + */ +const exportSnippetFilesBeta = async (pState: SnippetProcessingState) => { + const sPrefix = "Saving individual snippet files"; + const spinner = ora(`${sPrefix}…\n`).start(); + try { + // Delete the previous directory contents + // Write the snippets + for (let filename in pState.output_strings) { + let snippet: string = pState.output_strings[filename]; + spinner.text = `${sPrefix}: ${filename}`; + writeStrToFile(snippet, join(SNIPPET_SRC_DIR, filename)); + } + // Finish + spinner.succeed(`Individual snippet files are saved.`); + } catch (writeError) { + spinner.fail(`Unable to save the individual snippet files`); + } +}; + +/** + * Executes all currently required steps for + * building the documentation. + */ +export async function main() { + let pState: SnippetProcessingState = { + error: null, + output_dir_accessible: false, + sources: SOURCES, + parsing_result: null, + parsed: [], + output_strings: {} + }; + await validateSrcList(pState); + await printSourceList(pState); + await collectPagesBeta(pState); + await parsePagesBeta(pState); + await printAvailableSnippetsBeta(pState); + await ensureSnippetDirBeta(pState); + await setSnippetNames(pState); + await saveSnippetMeta(pState); + await exportSnippetFilesBeta(pState); +} diff --git a/scripts/get_snippets_runner.mts b/scripts/get_snippets_runner.mts new file mode 100755 index 000000000..10af8633b --- /dev/null +++ b/scripts/get_snippets_runner.mts @@ -0,0 +1,8 @@ +#!/usr/bin/env tsx + +/* + * A runner script for the snippet downloader. + */ + +import { main } from "./get_snippets.mjs"; +await main(); diff --git a/scripts/pre.mts b/scripts/pre.mts new file mode 100644 index 000000000..550be6a80 --- /dev/null +++ b/scripts/pre.mts @@ -0,0 +1,87 @@ +/** + * @module Prebuild + * + * This file checks that the examples are available + * before the build process is starting. + * + * It would automatically download the examples for CI + * and would interact with the user when a normal build happens. + */ + +import ci from "ci-info"; +import ora from "ora"; +import { SNIPPET_SRC_DIR } from "./constants.mjs"; +import { ensureDirExists, checkDirHasContents } from "./file_utils.mjs"; +import { main as get_examples_main } from "./get_snippets.mjs"; + +export type PreprocessingState = { + error: Error | null; + outputDirHasContents: boolean; +}; + +/** + * Checks if the snippet output directory exists, + * creates it if it doesn't. + * + * @param {PreprocessingState} pState - current execution state + */ +let ensureOutputDirAccessible = async (pState: PreprocessingState) => { + const spinner = ora({ + isEnabled: !ci.isCI, + text: "Locating the output file…" + }).start(); + try { + ensureDirExists(SNIPPET_SRC_DIR); + spinner.succeed("An output directory is available."); + } catch (ode) { + spinner.fail( + "Unable to access or create the output directory.\n" + ode.message + ); + } +}; + +/** + * Checks if the snippet output directory contains any files. + * + * @param {PreprocessingState} pState - current execution state + */ +let ensureOutputDirHasContents = async (pState: PreprocessingState) => { + const spinner = ora({ + isEnabled: !ci.isCI, + text: "Checking if the output directory contains snippets…" + }).start(); + pState.outputDirHasContents = checkDirHasContents(SNIPPET_SRC_DIR); + if (pState.outputDirHasContents) { + spinner.succeed("Snippet directory has contents."); + } else { + spinner.fail("Snippet directory is empty."); + } +}; + +/** + * Updates the contents of an output ESM JS file. + * + * @param pState - current execution state + */ +let updateOutputIfNotExists = async (pState: PreprocessingState) => { + if (!pState.outputDirHasContents) { + console.log("Downloading the examples.\n"); + await get_examples_main(); + } +}; + +/** + * Makes a Proc class instance execute + * all the required steps for a prebuilder code. + * + * @export + */ +export async function preMain() { + let pState: PreprocessingState = { + error: null, + outputDirHasContents: false + }; + await ensureOutputDirAccessible(pState); + await ensureOutputDirHasContents(pState); + await updateOutputIfNotExists(pState); +} diff --git a/scripts/pre_runner.mts b/scripts/pre_runner.mts new file mode 100755 index 000000000..c34c92316 --- /dev/null +++ b/scripts/pre_runner.mts @@ -0,0 +1,8 @@ +#!/usr/bin/env tsx + +/* + * A script that runs the snippet downloader. + */ + +import { preMain } from "./pre.mjs"; +await preMain(); diff --git a/scripts/types.mts b/scripts/types.mts new file mode 100644 index 000000000..07effe8a4 --- /dev/null +++ b/scripts/types.mts @@ -0,0 +1,42 @@ +/** + * Defines a snippet source + * + * + */ +export type SourceDefinition = { + // The software version to display in a tab title + version: string; + // A url to query and parse + url: string; + // A language highlight mode + lang: string; + // Text content of the page + content: string | undefined; +}; + +/** + * Defines an individual snippet + * + * @typedef {object} IndividualSnippet + */ +export type IndividualSnippet = { + name: string; + text: string; + lang: string; + url: string; + version: string; +}; + +/** + * Defines the state of the snippet retrieval + * + * @typedef {object} SnippetProcessingState + */ +export type SnippetProcessingState = { + output_dir_accessible: boolean; + error: Error | null; + sources: SourceDefinition[]; + parsing_result: any; + output_strings: Record; + parsed: IndividualSnippet[]; +}; diff --git a/scripts/util.mts b/scripts/util.mts new file mode 100644 index 000000000..9bb425672 --- /dev/null +++ b/scripts/util.mts @@ -0,0 +1,94 @@ +/** + * @module Utilities + * + * Common utilities, used by the documentation scripts. + */ + +import { join } from "path"; +import axios from "axios"; +import { IndividualSnippet, SourceDefinition } from "./types.mjs"; +import { pathToStr } from "./file_utils.mjs"; +import { langExtensions } from "./constants.mjs"; + +/** + * Checks if the source definitions are formatted correctly + * + * @param sources - a list of sources to be validated + */ +export const validateSources = (sources: SourceDefinition[]): true | Error => { + let result: true | Error; + if (sources.constructor.name == "Array") { + if (sources.length === 0) { + result = new Error("URL list should not be empty"); + } else { + result = true; + } + } else if (sources instanceof Error) result = sources; + else result = new Error("URL list should contain a list"); + return result; +}; + +/** + * Retrieves a URL. Supports http, https and file URLs. + * + * @param {SourceDefinition} source - definition for a source to retrieve a page, FS or not + * @param {string} lookupPath - a path for file contents to support relative and absolute paths + * @returns {Promise} + */ +export const collectPage = async ( + source: SourceDefinition, + lookupPath: string +): Promise => { + let result: SourceDefinition | Error; + if (source.url.startsWith("http://") || source.url.startsWith("https://")) { + const resp: string | Error = await axios({ + method: "get", + url: source.url + }) + .then((resp) => { + return resp.data; + }) + .catch((error) => { + return new Error( + `Unable to retrieve a page.\n` + + `HTTP status: ${error.response.status}.\n` + + `Status text: ${error.response.statusText}.` + ); + }); + result = Object.assign(source, { content: resp }); + } else if (source.url.startsWith("/")) { + const fileContent = pathToStr(source.url); + result = Object.assign(source, { content: fileContent }); + } else if (source.url.startsWith(".")) { + let resolvedPath: string = join(lookupPath, source.url.slice(2)); + const fileContent = pathToStr(resolvedPath); + result = Object.assign(source, { content: fileContent }); + } else { + result = new Error(`Wrong URL format: ${source.url}`); + } + return result; +}; + +/** + * Converts a language name to file extension; case—insensitive. + * + * @param {string} lang - a language name + * @returns {string} a file extension appropriate for a given language + */ +export const langToFileExt = (lang: string): string => { + const ll = lang.toLowerCase(); + return langExtensions.hasOwnProperty(ll) ? langExtensions[ll] : ""; +}; + +/** + * Formats an individual snippet filename + * + * @param {IndividualSnippet} snippet - snippet data needed from formatting + * @returns {string} snippet filename with an extension + */ +export const getSnippetFilename = function ( + snippet: IndividualSnippet +): string { + let ext = langToFileExt(snippet.lang); + return `${snippet.version}_${snippet.lang}_${snippet.name}.${ext}`; +}; diff --git a/snippet_sources.json b/snippet_sources.json new file mode 100644 index 000000000..5f23a5117 --- /dev/null +++ b/snippet_sources.json @@ -0,0 +1,32 @@ +[ + { + "version": "debug", + "url": "./src/example_code/lorem.rs", + "lang": "rust" + }, + { + "version": "debug", + "url": "./src/example_code/lorem.py", + "lang": "python" + }, + { + "version": "debug", + "url": "./src/example_code/lorem.js", + "lang": "javascript" + }, + { + "version": "debug", + "url": "./src/example_code/lorem.ts", + "lang": "typescript" + }, + { + "version": "debug", + "url": "./src/example_code/ILorem.java", + "lang": "java" + }, + { + "version": "debug", + "url": "./src/example_code/lorem.sh", + "lang": "shell" + } +] diff --git a/src/documenting/snippets.md b/src/documenting/snippets.md new file mode 100644 index 000000000..d0ec3229e --- /dev/null +++ b/src/documenting/snippets.md @@ -0,0 +1,277 @@ +# Code snippets + +Iroha development happens around three important branches: +[`dev`](https://github.com/hyperledger/iroha/tree/iroha2-dev), +[`stable`](https://github.com/hyperledger/iroha/tree/iroha2-stable), and +[`LTS`](https://github.com/hyperledger/iroha/tree/iroha2-lts). + +With that in mind, documenting all the API versions manually has +limitations: at some point, the content in one of the branches will be +different compared to the documentation. Moreover, code in the +documentation may contain typos sometimes, and being able to run it won't +be guaranteed without testing. This raises questions about showing the +differences between branches for the new developers semi-automatically. + +Our solution is to use a custom syntax in the code comments to mark the +snippets with the [dst-parser](https://github.com/soramitsu/dst-parser). We +configure URLs in a custom JSON config file and automatically query the +related URLs both on local builds and CI. + +In addition, we're also using a custom +[markdown-it](https://github.com/markdown-it/markdown-it) plugin, so that +we can easily include the resulting snippets. + +## Workflow + +### Preparing the requirements + +After you run `pnpm install`, a prebuilder script will run to ensure that +an output directory for the snippets (`src/snippets`) exists. It will also +download the snippets if those are not available. + +Alternatively, you can run the prebuilder with the `pnpm run postinstall` +commands. + +### Getting snippets + +To download and convert the raw files, type: `pnpm run get_snippets`. + +This will run a script that: + +- downloads each snippet file in parallel +- parses them using a + [dst-parser](https://www.npmjs.com/package/dst-parser) and extracts + individual snippets +- exports the parsed snippets into the individual code files in the + `src/snippets` directory +- exports the JSON metadata file (`meta.json`) into the same directory + +The filename is formatted like this: `version_lang_name.extension`. + +This approach allows attaching all the snippets by their names from the +custom component of the project's documentation system. This component is +implemented using [Vue](https://en.wikipedia.org/wiki/Vue.js), which is +already used in the [VitePress](https://vitepress.vuejs.org/) documentation +generator and is called with a custom Markdown syntax described below in +this article. + +### Defining the sources for the documentation + +To collect the code, we're using a custom [pnpm](https://pnpm.io/) script; +it reads a configuration list from a file named `snippet_sources.json` in +the documentation root, obtaining a list of items, each of which contains: + +- `url`: the `URL` of a file to parse, it supports `http://`, `https://`, + and relative file URLs, and it is mainly used for + [GitHub](https://github.com/) raw files +- `lang`: a language to highlight using + [Shiki](https://github.com/shikijs/shiki/) +- `version`: a branch or Iroha version (for example, `stable`, `dev`, or + `lts`), which is used in file prefixes to distinguish between similar + snippet names + +Generally, we want two types of files to be used as documentation sources: + +- raw sources from [GitHub](https://github.com/), because they are easy to + parse and there's no additional markup +- source code files to use in this demo + +Let's check the contents of the `snippet_sources.json` example: + +```json +[ + { + "version": "stable", + "url": "https://raw.githubusercontent.com/username/project/stable/examples/filename.rs", + "lang": "rust" + }, + { + "version": "dev", + "url": "https://raw.githubusercontent.com/username/project/dev/examples/filename.rs", + "lang": "rust" + }, + { + "version": "lts", + "url": "https://raw.githubusercontent.com/username/project/lts/examples/filename.rs", + "lang": "rust" + } +] +``` + +We could have many source definitions[^1] inside this list, but each +definition, represented as a dictionary, is required to have the properties +displayed above: `version`, `url`, `lang`. At a later date, automatic +language detection may be added. + +## Code comment syntax + +Currently, the [dst-parser](https://github.com/soramitsu/dst-parser) +supports two comment formats: C-like (`//`) and Pythonic (`#`). Multiline +comments (`/* … */` and `""" … """`) are not parsed. The supported +languages are Rust, C, C++, Java, JavaScript, and Python. + +A piece of code is considered a named fragment when it is located between +`// BEGIN FRAGMENT: ` and `// END FRAGMENT`, where `<>` signs are not +included. This syntax is case-sensitive. Names support alphanumeric +characters, underscores, minus signs, and white spaces. + +Fragments can be included in one another. In that case, the lines matching +`// BEGIN FRAGMENT: ` and `// END FRAGMENT` are removed. + +Elements between `// BEGIN ESCAPE` and `// END ESCAPE` are excluded +unconditionally from the tutorial. + +While defining code documentation, you can use both underscores and white +spaces. It is preferable to use white spaces because they're used for +prefixing branch names. + +The current version of the snippet collection script supports both normal +URLs and file paths[^2]. + +Considering the current design, specifically the layout and font +configuration, the optimal width for doc comments is 66 characters, +starting with a comment symbol(s). If there's common padding behind each +line, it is also removed. If the content is too long, it won't fit normally +and a scrollbar will appear. + +## Using the custom Markdown syntax + +With the [dst-parser](https://github.com/soramitsu/dst-parser) and +downloader doing their parts of the task, it is possible to import the +snippets. A single file that was added may contain more than one example. +The syntax may look like this: + +``` +::snippets +debug_rust_Lorem.rs +debug_typescript_Lorem.ts +:: +``` + +For now, there's an +[issue](https://github.com/prettier/prettier/issues/13512) that requires an +override for a custom syntax, otherwise the formatting check and +autoformatting can't be used: + +``` + +::snippets +debug_rust_Lorem.rs +debug_typescript_Lorem.ts +:: + + +``` + +In this example, our snippet files (`A.rs`, `B.py`, `C.js`) are located in +a default snippet directory: `src/snippets/`. + +One can include the snippets from different directories for debugging +purposes if needed, using a path relative to the directory containing the +snippets: + +``` + +::snippets +../alt_path/debug_java_Lorem.java +../alt_path/debug_python_Lorem.py +:: + + +``` + +The Markdown parser part is separated from the +[dst-parser](https://www.npmjs.com/package/dst-parser) so that each file is +not requested and parsed only a single time. + +A [markdown-it](https://github.com/markdown-it/markdown-it) parser plugin +outputs a code for an internal +[Vue component](https://vuejs.org/guide/essentials/component-basics.html), +`SnippetTabs`. This plugin is based on a +[container](https://github.com/markdown-it/markdown-it-container) plugin. +It is called by [VitePress](https://vitepress.vuejs.org/) to display the +result. + +This [markdown-it](https://github.com/markdown-it/markdown-it) plugin needs +the `meta.json` file mentioned above to add the metadata to the tabs and +the metadata can be extended. + +## Demo + + +::snippets +debug_java_Lorem.java +debug_python_Lorem.py +debug_javascript_Lorem.js +debug_typescript_Lorem.ts +debug_rust_Lorem.rs +debug_shell_Lorem.sh +:: + + + +## Troubleshooting + +### Missing files + +Sometimes, you may encounter an error while running documentation builds or +the development mode. + +``` +SnippetAccessError: Unable to read a file. +Ensure it exists, its location is correct and its access rights allow to read it. +Filename: "snippet_x.rs". +Directory path: "…src/snippets". +``` + +This error will be displayed with both the `pnpm run build` and +`pnpm run dev` commands. These details are necessary for the documentation +quality, so it won't build without such errors being resolved. + +To resolve this error, rebuild the snippets with the +`pnpm run get_snippets` command. A new file should appear in +`src/snippets`. + +If there is no new file, make sure that `snippet_sources.json` contains the +path to that snippet. Also, make sure the doc comment in the said file +matches the name in the snippet tabs definition. + +## Internals + +### Vue tab component + +The custom component file that displays tabs is called `SnippetTabs.vue`. +It is located in the `.vitepress/theme/components/` directory. + +```typescript +import SnippetTabs from './components/SnippetTabs.vue' +// … +export default { + // … + enhanceApp({ app }) { + app.component('SnippetTabs', SnippetTabs) + }, +} +``` + +### Parser plugin integration + +The Markdown parser section is enabled in `.vitepress/config.ts`, in the +`markdown` → `config` section: + +```javascript +{ + markdown: { + config(md) { + md.use(footnote); + snippets_plugin(md, {'snippet_root': resolve(__dirname, '../src/snippets/')}) + } + } +} +``` + +Note that `snippet_root` directory path is required, otherwise the user has +to point out the paths. + +[^1]: JSON dictionaries with `version`, `url`, and `lang` parameters +[^2]: which were only tested on Linux diff --git a/src/example_code/ILorem.java b/src/example_code/ILorem.java new file mode 100644 index 000000000..c378b54a8 --- /dev/null +++ b/src/example_code/ILorem.java @@ -0,0 +1,7 @@ +// BEGIN FRAGMENT: Lorem +class ILorem { + public static void main(String[] args) { + System.out.println("Lorem ipsum"); + } +} +// END FRAGMENT diff --git a/src/example_code/lorem.js b/src/example_code/lorem.js new file mode 100644 index 000000000..a052d144b --- /dev/null +++ b/src/example_code/lorem.js @@ -0,0 +1,3 @@ +// BEGIN FRAGMENT: Lorem +console.log('Lorem ipsum') +// END FRAGMENT diff --git a/src/example_code/lorem.py b/src/example_code/lorem.py new file mode 100644 index 000000000..55b379668 --- /dev/null +++ b/src/example_code/lorem.py @@ -0,0 +1,3 @@ +# BEGIN FRAGMENT: Lorem +print('Lorem ipsum') +# END FRAGMENT diff --git a/src/example_code/lorem.rs b/src/example_code/lorem.rs new file mode 100644 index 000000000..3641b4432 --- /dev/null +++ b/src/example_code/lorem.rs @@ -0,0 +1,5 @@ +// BEGIN FRAGMENT: Lorem +fn main() { + println!("Lorem ipsum"); +} +// END FRAGMENT diff --git a/src/example_code/lorem.sh b/src/example_code/lorem.sh new file mode 100644 index 000000000..e72bf1f5a --- /dev/null +++ b/src/example_code/lorem.sh @@ -0,0 +1,4 @@ +# BEGIN FRAGMENT: Lorem +#!/bin/bash +echo "Lorem ipsum" +# END FRAGMENT diff --git a/src/example_code/lorem.ts b/src/example_code/lorem.ts new file mode 100644 index 000000000..716cc8399 --- /dev/null +++ b/src/example_code/lorem.ts @@ -0,0 +1,3 @@ +// BEGIN FRAGMENT: Lorem +console.log('Lorem ipsum'); +// END FRAGMENT