diff --git a/.env b/.env new file mode 100644 index 000000000..b20e07093 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +VITE_FEEDBACK_URL=https://164.92.190.45/feedback/form +VITE_COMPAT_MATRIX_URL=https://docs-compat.iroha2.tachi.soramitsu.co.jp/compat-matrix diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..a7fd5fc17 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +.vitepress/dist +.vitepress/cache +src/snippets diff --git a/.eslintrc.js b/.eslintrc.js index 50d0d3aab..c1e88f677 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,15 +1,29 @@ +// eslint-disable-next-line no-undef module.exports = { - extends: ['plugin:vue'], - // env: { - // es2021: true, - // }, - rules: { - 'spaced-comment': [ - 'error', - 'always', - { - markers: ['/'], - }, - ], + root: true, + extends: ['plugin:vue/vue3-recommended', 'eslint:recommended', 'plugin:@typescript-eslint/recommended'], + parser: 'vue-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser', + sourceType: 'module', + }, + rules: { + 'vue/html-indent': ['error', 2], + 'spaced-comment': [ + 'error', + 'always', + { + markers: ['/'], + }, + ], + }, + overrides: [ + { + files: ['.vitepress/theme/components/MermaidRender.vue'], + rules: { + // FIXME: find a way to disable this it for the particular line + 'vue/no-v-html': 'off', + }, }, + ], } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..d93597c6a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,14 @@ +* @outoftardis + +# doc sources +/src/ @outoftardis @WRRicht3r @nxsaken +*.md @outoftardis @WRRicht3r @nxsaken +src/guide/javascript.md @0x009922 @outoftardis @WRRicht3r @nxsaken + +# configurations +/.vitepress/ @0x009922 +*.js @0x009922 +*.ts @0x009922 +*.json @0x009922 +*.yaml @0x009922 +*.vue @0x009922 diff --git a/.github/workflows/gh-pages-deploy.yaml b/.github/workflows/gh-pages-deploy.yaml new file mode 100644 index 000000000..1d9e7fff1 --- /dev/null +++ b/.github/workflows/gh-pages-deploy.yaml @@ -0,0 +1,68 @@ +name: Deploy at GitHub Pages + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + node-version: 18 + + # --- Install with caching + # https://github.com/pnpm/action-setup#use-cache-to-reduce-installation-time + + - name: Enable Corepack + run: corepack enable + + - name: Get pnpm store directory + id: pnpm-cache + run: | + echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: | + ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} + /home/runner/.cache/Cypress + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install packages + run: pnpm install + + - name: Check formatting + run: pnpm format:check + + - name: Run linter + run: pnpm lint + + - name: Run tests + run: pnpm test + + - name: Build VitePress + run: | + pnpm build + pnpm cli validate-links .vitepress/dist --public-path $PUBLIC_PATH + env: + PUBLIC_PATH: /iroha-2-docs/ + # chalk has a color detection bug + FORCE_COLOR: 2 + + - name: Push static content into master:gh-pages + working-directory: .vitepress/dist + run: | + git config --global user.email "${GITHUB_ACTOR}@https://users.noreply.github.com/" + git config --global user.name "${GITHUB_ACTOR}" + git init + git add --all + git commit -m "Auto update pages on $(date +'%Y-%m-%d %H:%M:%S')" + git push -f -q https://git:${{ secrets.github_token }}@github.com/${{ github.repository }} master:gh-pages diff --git a/.github/workflows/pull-request-ci.yaml b/.github/workflows/pull-request-ci.yaml new file mode 100644 index 000000000..dcd411472 --- /dev/null +++ b/.github/workflows/pull-request-ci.yaml @@ -0,0 +1,58 @@ +name: Pull Request CI +on: + pull_request: + branches: [main] +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 18 + + # --- Install with caching + # https://github.com/pnpm/action-setup#use-cache-to-reduce-installation-time + + - name: Enable Corepack + run: corepack enable + + - name: Get pnpm store directory + id: pnpm-cache + run: | + echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: | + ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} + /home/runner/.cache/Cypress + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install packages + run: pnpm install + + - name: Type check + run: pnpm typecheck + + - name: Lint + run: pnpm lint + + - name: Format + run: pnpm format:check + + - name: Run tests + run: pnpm test + + - name: Build + run: pnpm build + + - name: Validate links + run: pnpm cli validate-links .vitepress/dist + env: + # chalk has a color detection bug + FORCE_COLOR: 2 diff --git a/.gitignore b/.gitignore index f06235c46..989426f81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ +.DS_Store node_modules dist +/src/flymd.md +/src/flymd.html +/src/*.temp +/src/snippets +.vitepress/cache +.idea diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..a65759dba --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@iroha2:registry=https://nexus.iroha.tech/repository/npm-group/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..f2d21b03a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +.vitepress/dist +.vitepress/cache +/src/snippets +/src/example_code diff --git a/.prettierrc.js b/.prettierrc.js index e084c13df..2053034c0 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,39 +1,32 @@ +// eslint-disable-next-line no-undef module.exports = { - // max 120 characters per line - printWidth: 120, - // use 24spaces for indentation - tabWidth: 4, - // use spaces instead of indentations - useTabs: false, - // semicolon at the end of the line - semi: false, - // use single quotes - singleQuote: true, - // object's key is quoted only when necessary - quoteProps: 'as-needed', - // use double quotes instead of single quotes in jsx - jsxSingleQuote: false, - // no comma at the end - trailingComma: 'all', - // spaces are required at the beginning and end of the braces - bracketSpacing: true, - // brackets are required for arrow function parameter, even when there is only one parameter - arrowParens: 'always', - // format the entire contents of the file - rangeStart: 0, - rangeEnd: Infinity, - // no need to write the beginning @prettier of the file - requirePragma: false, - // No need to automatically insert @prettier at the beginning of the file - insertPragma: false, - // use default break criteria - proseWrap: 'preserve', - // decide whether to break the html according to the display style - htmlWhitespaceSensitivity: 'css', - // vue files script and style tags indentation - vueIndentScriptAndStyle: false, - // lf for newline - endOfLine: 'lf', - // formats quoted code embedded - embeddedLanguageFormatting: 'auto', + printWidth: 120, + tabWidth: 2, + useTabs: false, + semi: false, + singleQuote: true, + quoteProps: 'as-needed', + jsxSingleQuote: false, + trailingComma: 'all', + bracketSpacing: true, + arrowParens: 'always', + rangeStart: 0, + rangeEnd: Infinity, + requirePragma: false, + insertPragma: false, + proseWrap: 'preserve', + htmlWhitespaceSensitivity: 'css', + vueIndentScriptAndStyle: false, + endOfLine: 'lf', + embeddedLanguageFormatting: 'auto', + + overrides: [ + { + files: ['./src/**/*.md'], + options: { + printWidth: 75, + proseWrap: 'always', + }, + }, + ], } diff --git a/.vitepress/config.mts b/.vitepress/config.mts new file mode 100644 index 000000000..a19463ad6 --- /dev/null +++ b/.vitepress/config.mts @@ -0,0 +1,482 @@ +/// + +import { defineConfig, DefaultTheme } from 'vitepress' +import footnote from 'markdown-it-footnote' +import { resolve } from 'path' +import ViteSvgLoader from 'vite-svg-loader' +import ViteUnoCSS from 'unocss/vite' +import { mermaid } from './md-mermaid' +import { katex } from '@mdit/plugin-katex' + +function nav(): DefaultTheme.NavItem[] { + return [ + { + text: 'Get Started', + link: '/get-started/', + activeMatch: '/get-started/', + }, + { + text: 'Build on Iroha', + link: '/guide/tutorials/', + activeMatch: '/guide/', + }, + { + text: 'Iroha Explained', + link: '/blockchain/iroha-explained', + activeMatch: '/blockchain/', + }, + { + text: 'Reference', + link: '/reference/torii-endpoints', + activeMatch: '/reference/', + }, + { + text: 'Help', + link: '/help/', + activeMatch: '/help/', + }, + ] +} + +function sidebarStart(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'Get Started', + link: '/get-started/', + items: [ + { + text: 'Install Iroha', + link: '/get-started/install-iroha-2', + }, + { + text: 'Launch Iroha', + link: '/get-started/launch-iroha-2', + }, + { + text: 'Operate Iroha via CLI', + link: '/get-started/operate-iroha-2-via-cli', + }, + { + text: 'Iroha 2 vs. Iroha 1', + link: '/get-started/iroha-2', + }, + ], + }, + ] +} + +function sidebarGuide(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'SDK Tutorials', + link: '/guide/tutorials/', + collapsed: false, + items: [ + /* a common lang-agnostic section will go here */ + { + text: 'Rust', + link: '/guide/tutorials/rust', + }, + { + text: 'Python 3', + link: '/guide/tutorials/python', + }, + { + text: 'Kotlin/Java', + link: '/guide/tutorials/kotlin-java', + }, + { + text: 'JavaScript', + link: '/guide/tutorials/javascript', + }, + ], + }, + { + text: 'Configuration and Management', + collapsed: false, + items: [ + { + text: 'Configure Iroha', + collapsed: true, + items: [ + { + text: 'Configuration Types', + link: '/guide/configure/configuration-types', + }, + { + text: 'Samples', + link: '/guide/configure/sample-configuration', + }, + { + text: 'Peer Configuration', + link: '/guide/configure/peer-configuration', + }, + { + text: 'Client Configuration', + link: '/guide/configure/client-configuration', + }, + { + text: 'Genesis Block', + link: '/guide/configure/genesis', + }, + { + text: 'Metadata and Store assets', + link: '/guide/configure/metadata-and-store-assets', + }, + ], + }, + { + text: 'Keys for Network Deployment', + link: '/guide/configure/keys-for-network-deployment.md', + }, + { + text: 'Peer Management', + link: '/guide/configure/peer-management', + }, + { + text: 'Public and Private Blockchains', + link: '/guide/configure/modes', + }, + ], + }, + { + text: 'Security', + link: '/guide/security/', + collapsed: false, + items: [ + { + text: 'Security Principles', + link: '/guide/security/security-principles.md', + }, + { + text: 'Operational Security', + link: '/guide/security/operational-security.md', + }, + { + text: 'Password Security', + link: '/guide/security/password-security.md', + }, + { + text: 'Public Key Cryptography', + collapsed: true, + link: '/guide/security/public-key-cryptography.md', + items: [ + { + text: 'Generating Cryptographic Keys', + link: '/guide/security/generating-cryptographic-keys.md', + }, + { + text: 'Storing Cryptographic Keys', + link: '/guide/security/storing-cryptographic-keys.md', + }, + ], + }, + ], + }, + { + text: 'Advanced Use Cases', + collapsed: false, + items: [ + { + text: 'Iroha On Bare Metal', + link: '/guide/advanced/running-iroha-on-bare-metal', + }, + { + text: 'Hot Reload Iroha', + link: '/guide/advanced/hot-reload', + }, + { + text: 'Monitor Iroha Performance', + link: '/guide/advanced/metrics', + }, + ], + }, + /* { + text: 'Reports', + collapsed: true, + items: [ + { + text: 'CSD/RTGS linkages via on-chain scripting', + link: '/guide/reports/csd-rtgs', + }, + ], + }, +*/ + ] +} + +function sidebarChain(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'Iroha Explained', + link: '/blockchain/iroha-explained', + items: [ + { + text: 'Overview', + items: [ + { + text: 'Transactions', + link: '/blockchain/transactions', + }, + { + text: 'Consensus', + link: '/blockchain/consensus', + }, + { + text: 'Data Model', + link: '/blockchain/data-model', + }, + ], + }, + { + text: 'Entities', + items: [ + { + text: 'Assets', + link: '/blockchain/assets', + }, + /* + { + text: 'Accounts', + link: '/blockchain/accounts', + }, + { + text: 'Domains', + link: '/blockchain/domains', + }, + */ + { + text: 'Metadata', + link: '/blockchain/metadata', + }, + { + text: 'Events', + link: '/blockchain/events', + }, + { + text: 'Filters', + link: '/blockchain/filters', + }, + { + text: 'Triggers', + link: '/blockchain/triggers', + items: [ + { + text: 'Event Triggers by Example', + link: '/blockchain/trigger-examples', + }, + ], + }, + { + text: 'Queries', + link: '/blockchain/queries', + }, + { + text: 'Permissions', + link: '/blockchain/permissions', + }, + { + text: 'World', + link: '/blockchain/world', + }, + ], + }, + { + text: 'Operations', + items: [ + { + text: 'Instructions', + link: '/blockchain/instructions', + }, + { + text: 'Expressions', + link: '/blockchain/expressions', + }, + { + text: 'Web Assembly', + link: '/blockchain/wasm', + }, + ], + }, + ], + }, + ] +} + +function sidebarAPI(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'About', + items: [ + { + text: 'Glossary', + link: '/reference/glossary.md', + }, + { + text: 'Naming Conventions', + link: '/reference/naming.md', + }, + { + text: 'Compatibility Matrix', + link: '/reference/compatibility-matrix', + }, + { + text: 'Foreign Function Interfaces', + link: '/reference/ffi', + }, + ], + }, + { + text: 'Reference', + items: [ + { + text: 'Torii Endpoints', + link: '/reference/torii-endpoints.md', + }, + { + text: 'Data Model Schema', + link: '/reference/data-model-schema', + }, + { + text: 'Instructions', + link: '/reference/instructions', + }, + { + text: 'Queries', + link: '/reference/queries.md', + }, + { + text: 'Permissions', + link: '/reference/permissions.md', + }, + ], + }, + ] +} + +function sidebarHelp(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'Receive support', + link: '/help/', + }, + { + text: 'Troubleshooting', + items: [ + { + text: 'Overview', + link: '/help/overview', + }, + { + text: 'Installation', + link: '/help/installation-issues', + }, + { + text: 'Configuration', + link: '/help/configuration-issues', + }, + { + text: 'Deployment', + link: '/help/deployment-issues', + }, + { + text: 'Integration', + link: '/help/integration-issues', + }, + ], + }, + ] +} + +const BASE = process.env.PUBLIC_PATH ?? '/' + +export default defineConfig({ + base: BASE, + srcDir: 'src', + srcExclude: ['snippets/*.md'], + title: "Hyperledger Iroha 2 Docs | World's Most Advanced Blockchain Framework", + description: + 'Documentation for Hyperledger Iroha 2 offering step-by-step guides for SDKs and outlining the main differences between Iroha versions.', + lang: 'en-US', + vite: { + plugins: [ViteUnoCSS('../uno.config.ts'), ViteSvgLoader()], + envDir: resolve(__dirname, '../'), + }, + lastUpdated: true, + + head: [ + // Based on: https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs + ['link', { rel: 'icon', href: BASE + 'favicon.ico', sizes: 'any' }], + ['link', { rel: 'icon', href: BASE + 'icon.svg', sizes: 'image/svg+xml' }], + ['link', { rel: 'apple-touch-icon', href: BASE + 'apple-touch-icon.png' }], + ['link', { rel: 'manifest', href: BASE + 'manifest.webmanifest' }], + // Google Analytics integration + ['script', { src: 'https://www.googletagmanager.com/gtag/js?id=G-D6ETK9TN47' }], + [ + 'script', + {}, + ` + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + + gtag('config', 'G-D6ETK9TN47'); + `, + ], + // KaTeX stylesheet + ['link', { rel: 'stylesheet', href: 'https://esm.sh/katex@0.16.8/dist/katex.min.css' }], + ], + + markdown: { + async config(md) { + md.use(footnote) + .use(mermaid) + // Note: Since vitepress@1.0.0-rc.14, it supports MathJax natively with `markdown.math = true`: + // https://github.com/vuejs/vitepress/pull/2977 + // Although KaTeX is more efficient, we might consider removing it in the future. + .use(katex) + }, + }, + + themeConfig: { + logo: '/icon.svg', + siteTitle: 'Iroha 2', + + socialLinks: [ + { icon: 'github', link: 'https://github.com/hyperledger-iroha/iroha-2-docs' }, + { + icon: { + /** + * https://icones.js.org/collection/material-symbols?s=bug + */ + svg: ``, + }, + link: 'https://github.com/hyperledger-iroha/iroha-2-docs/issues/new', + }, + ], + + editLink: { + pattern: 'https://github.com/hyperledger-iroha/iroha-2-docs/edit/main/src/:path', + text: 'Edit this page on GitHub', + }, + + lastUpdated: { + text: 'Last Updated', + }, + + nav: nav(), + outline: [2, 3], + + sidebar: { + '/get-started/': sidebarStart(), + '/guide/': sidebarGuide(), + '/blockchain/': sidebarChain(), + '/reference/': sidebarAPI(), + '/help/': sidebarHelp(), + }, + + search: { + provider: 'local', + }, + }, +}) diff --git a/.vitepress/config.ts b/.vitepress/config.ts deleted file mode 100644 index 189af52aa..000000000 --- a/.vitepress/config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineConfigWithTheme } from 'vitepress' - -export default defineConfigWithTheme({ - srcDir: 'src', -}) diff --git a/.vitepress/md-mermaid.ts b/.vitepress/md-mermaid.ts new file mode 100644 index 000000000..222131391 --- /dev/null +++ b/.vitepress/md-mermaid.ts @@ -0,0 +1,26 @@ +import MarkdownIt from 'markdown-it' +import hasha from 'hasha' + +/** + * Mermaid-js markdown-it plugin + */ +export const mermaid = (md: MarkdownIt) => { + const fence = md.renderer.rules.fence.bind(md.renderer.rules) + + md.renderer.rules.fence = (tokens, index, options, env, slf) => { + const token = tokens[index] + + if (token.info.trim() === 'mermaid') { + const content = token.content.trim() + const id = `mermaid_${hasha(content)}` + return `` + } + + // Shiki will highlight `mmd` as `mermaid` + if (token.info.trim() === 'mmd') { + tokens[index].info = 'mermaid' + } + + return fence(tokens, index, options, env, slf) + } +} diff --git a/.vitepress/theme/components/CompatibilityMatrixTable.vue b/.vitepress/theme/components/CompatibilityMatrixTable.vue new file mode 100644 index 000000000..fe377806d --- /dev/null +++ b/.vitepress/theme/components/CompatibilityMatrixTable.vue @@ -0,0 +1,104 @@ + + + + + + + {{ name }} + + + + + {{ row.story }} + + + + + + + + + + Loading data... + + + Failed to load compatibility matrix data: {{ task.state.rejected.reason }} + + + + + diff --git a/.vitepress/theme/components/CompatibilityMatrixTableIcon.vue b/.vitepress/theme/components/CompatibilityMatrixTableIcon.vue new file mode 100644 index 000000000..4a895f98a --- /dev/null +++ b/.vitepress/theme/components/CompatibilityMatrixTableIcon.vue @@ -0,0 +1,57 @@ + + + + + + + diff --git a/.vitepress/theme/components/LayoutCustom.vue b/.vitepress/theme/components/LayoutCustom.vue new file mode 100644 index 000000000..2be2e3235 --- /dev/null +++ b/.vitepress/theme/components/LayoutCustom.vue @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/.vitepress/theme/components/MermaidRender.vue b/.vitepress/theme/components/MermaidRender.vue new file mode 100644 index 000000000..f6dbcc7df --- /dev/null +++ b/.vitepress/theme/components/MermaidRender.vue @@ -0,0 +1,71 @@ + + + + + + + + + + Unable to render the diagram + + {{ taskState.rejected.reason }} + + + {{ textDecoded }} + + + + + + Rendering the diagram... + + + diff --git a/.vitepress/theme/components/MermaidRenderWrap.vue b/.vitepress/theme/components/MermaidRenderWrap.vue new file mode 100644 index 000000000..e7b4b20d5 --- /dev/null +++ b/.vitepress/theme/components/MermaidRenderWrap.vue @@ -0,0 +1,16 @@ + + + + + + + diff --git a/.vitepress/theme/components/ShareFeedback.vue b/.vitepress/theme/components/ShareFeedback.vue new file mode 100644 index 000000000..4f9f88888 --- /dev/null +++ b/.vitepress/theme/components/ShareFeedback.vue @@ -0,0 +1,260 @@ + + + + + + Share feedback + + + + + + + + + + Share feedback + + + + + + + + + + + Thank you for sharing your feedback! + + + + + Close + + + + + + + + Please take a moment to help us improve the Iroha 2 Documentation. We take your input very seriously. + + + + + + Feedback type* + + + + + {{ KINDS_LABELS[value] }} + + + + + + Feedback* + + + + + + (optional) Contact information + + + + + + + Unable to send feedback + + + + + + Cancel + + + Submit + + + + + + + + + diff --git a/.vitepress/theme/components/VBtnPrimary.vue b/.vitepress/theme/components/VBtnPrimary.vue new file mode 100644 index 000000000..dfb6bddf6 --- /dev/null +++ b/.vitepress/theme/components/VBtnPrimary.vue @@ -0,0 +1,19 @@ + + + + + + + diff --git a/.vitepress/theme/components/VBtnSecondary.vue b/.vitepress/theme/components/VBtnSecondary.vue new file mode 100644 index 000000000..63e6d4bbb --- /dev/null +++ b/.vitepress/theme/components/VBtnSecondary.vue @@ -0,0 +1,5 @@ + + + + + diff --git a/.vitepress/theme/components/icons/IconCancelOutlineRounded.vue b/.vitepress/theme/components/icons/IconCancelOutlineRounded.vue new file mode 100644 index 000000000..a5d9456b1 --- /dev/null +++ b/.vitepress/theme/components/icons/IconCancelOutlineRounded.vue @@ -0,0 +1,19 @@ + + + + + + + diff --git a/.vitepress/theme/components/icons/IconCheck.vue b/.vitepress/theme/components/icons/IconCheck.vue new file mode 100644 index 000000000..41b18236a --- /dev/null +++ b/.vitepress/theme/components/icons/IconCheck.vue @@ -0,0 +1,19 @@ + + + + + + + diff --git a/.vitepress/theme/components/icons/IconClose.vue b/.vitepress/theme/components/icons/IconClose.vue new file mode 100644 index 000000000..4ca59ae89 --- /dev/null +++ b/.vitepress/theme/components/icons/IconClose.vue @@ -0,0 +1,19 @@ + + + + + + + diff --git a/.vitepress/theme/components/icons/IconFeedback.vue b/.vitepress/theme/components/icons/IconFeedback.vue new file mode 100644 index 000000000..6f9c456a4 --- /dev/null +++ b/.vitepress/theme/components/icons/IconFeedback.vue @@ -0,0 +1,19 @@ + + + + + + + diff --git a/.vitepress/theme/components/icons/IconQuestionMarkRounded.vue b/.vitepress/theme/components/icons/IconQuestionMarkRounded.vue new file mode 100644 index 000000000..1622035e3 --- /dev/null +++ b/.vitepress/theme/components/icons/IconQuestionMarkRounded.vue @@ -0,0 +1,19 @@ + + + + + + + diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts new file mode 100644 index 000000000..e923fafdb --- /dev/null +++ b/.vitepress/theme/index.ts @@ -0,0 +1,26 @@ +/// + +import ThemeDefault from 'vitepress/theme' +import { type EnhanceAppContext } from 'vitepress' +import LayoutCustom from './components/LayoutCustom.vue' +import MermaidRenderWrap from './components/MermaidRenderWrap.vue' + +import 'virtual:uno.css' +import './style/index.scss' +import { defineAsyncComponent } from 'vue' + +export default { + ...ThemeDefault, + Layout: LayoutCustom, + enhanceApp({ app }: EnhanceAppContext) { + app.component('MermaidRenderWrap', MermaidRenderWrap) + app.component( + 'CompatibilityMatrixTable', + defineAsyncComponent(() => import('./components/CompatibilityMatrixTable.vue')), + ) + app.component( + 'CompatibilityMatrixTableIcon', + defineAsyncComponent(async () => import('./components/CompatibilityMatrixTableIcon.vue')), + ) + }, +} diff --git a/.vitepress/theme/mermaid-cdn.d.ts b/.vitepress/theme/mermaid-cdn.d.ts new file mode 100644 index 000000000..0c75d4914 --- /dev/null +++ b/.vitepress/theme/mermaid-cdn.d.ts @@ -0,0 +1,3 @@ +declare module 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs' { + export { default } from 'mermaid' +} diff --git a/.vitepress/theme/mermaid-render.ts b/.vitepress/theme/mermaid-render.ts new file mode 100644 index 000000000..0bf1767bd --- /dev/null +++ b/.vitepress/theme/mermaid-render.ts @@ -0,0 +1,14 @@ +import './mermaid-cdn.d.ts' +import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs' + +export async function renderSvg( + id: string, + text: string, + options: { + theme: 'light' | 'dark' + }, +): Promise<{ svg: string }> { + mermaid.initialize({ startOnLoad: true, theme: options.theme }) + const { svg } = await mermaid.render(id, text) + return { svg } +} diff --git a/.vitepress/theme/style/index.scss b/.vitepress/theme/style/index.scss new file mode 100644 index 000000000..1d9c95fee --- /dev/null +++ b/.vitepress/theme/style/index.scss @@ -0,0 +1,47 @@ +@import url('https://fonts.googleapis.com/css2?family=Sora:wght@100..800&display=swap'); + +:root { + --vp-font-family-mono: 'JetBrains Mono', 'Fira Code', Menlo, Monaco, Consolas, 'Courier New', monospace; + --vp-font-family-base: 'Sora', 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + + --vp-c-brand-1: var(--vp-c-red-1); + --vp-c-brand-2: var(--vp-c-red-2); + --vp-c-brand-3: var(--vp-c-red-3); + --vp-c-brand-soft: var(--vp-c-red-soft); +} + +.VPSidebarItem.level-0 .text { + font-weight: 500 !important; + color: var(--vp-c-text-1); +} + +.VPSidebarItem.level-1 .text, .VPSidebarItem.level-2 .text, .VPSidebarItem.level-3 .text, .VPSidebarItem.level-4 .text, .VPSidebarItem.level-5 .text { + font-weight: 400 !important; + color: var(--vp-c-text-2); +} + +.VPHero .image { + min-height: 320px; +} + +.vp-doc { + div[class*='language-'] { + pre { + font-size: 14px; + font-variant-ligatures: none; + } + } + + code { + font-variant-ligatures: none; + } + + sup { + line-height: 0; + } + + p { + line-height: 1.75; + font-weight: 300; + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..d4a90eccc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eslint.workingDirectories": [".vitepress", "."] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 000000000..857e65af4 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,21 @@ +## Maintainers + +### Active Documentation writers + + +| name | Github | Discord | +|-----------------------|------------------------------------------------|--------------------| +| William Ruiz-Richter | [@WRRicht3r](https://github.com/WRRicht3r) | WRRichter#2242 | +| Ekaterina Mekhnetsova | [@outoftardis](https://github.com/outoftardis) | None | +| Victor Gridnevsky | [@6r1d](https://github.com/6r1d) | 6r1d#4829 | +| Aleksandr Petrosyan | [@appetrosyan](https://github.com/appetrosyan) | a-p-petrosyan#9734 | +| Bogdan Yamkovoy | [@yamkovoy](https://github.com/yamkovoy) | None | + + +### Website maintenance + + +| name | Github | Discord | +|---------------------|------------------------------------------------|--------------------| +| Dmitry Balashov | [@0x009922](https://github.com/0x009922) | None | +| Victor Gridnevsky | [@6r1d](https://github.com/6r1d) | 6r1d#4829 | diff --git a/README.md b/README.md new file mode 100644 index 000000000..591be24aa --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# Hyperledger Iroha 2 Tutorial + +This repository contains the source files for [Hyperledger Iroha 2 Documentation](https://docs.iroha.tech/). + +The tutorial is suitable for both experienced and novice users. It explains Iroha 2 concepts and features, and also offers language-specific step-by-step guides for these programming languages: + +- [CLI](https://docs.iroha.tech/get-started/operate-iroha-2-via-cli.html) +- [Python](https://docs.iroha.tech/guide/tutorials/python.html) +- [Rust](https://docs.iroha.tech/guide/tutorials/rust.html) +- [Kotlin/Java](https://docs.iroha.tech/guide/tutorials/kotlin-java.html) +- [Javascript (TypeScript)](https://docs.iroha.tech/guide/tutorials/javascript.html) + +If you are already familiar with Hyperledger Iroha, we invite you to read about [how Iroha 2 is different](https://docs.iroha.tech/get-started/iroha-2.html) from its previous version. + +Check the [Hyperledger Iroha](https://github.com/hyperledger-iroha/iroha/) repository for more detailed information about API and available features. + +## Contribution + +If you want to contribute to Iroha 2 tutorial, please clone the repository and follow the steps below. + +### Prepare the environment + +1. **Install Node.js v16.9+.** To install it without a headache, use [NVM](https://github.com/nvm-sh/nvm#installing-and-updating) (Node Version Manager). You can run something like this: + + ```bash + # Install NVM itself + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash + + # Run it to use NVM in the current shell session or restart your shell + export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm + ``` + + Then you can install Node 18: + + ```bash + nvm install 18 + ``` + +2. **Enable [Corepack](https://github.com/nodejs/corepack)**: + + ```bash + corepack enable + ``` + +3. **Install project dependencies.** From the root of the cloned repository, run: + + ```bash + pnpm install + ``` + +### Run dev mode + +```bash +pnpm dev +``` + +It will start a local dev-server. You will be able to open a browser, observe rendered documentation, edit source files and see your edits on-demand. + +### Formatting + +We use [Prettier](https://prettier.io/) to format project sources. Its configuration is located at `./.prettierrc.js`. Check [options reference](https://prettier.io/docs/en/options.html) for all available options. + +- **Format sources**: apply formatting to all project source files: + + ```bash + pnpm format:fix + ``` + +- **Check the formatting in sources**: ensure that all project source files match `Prettier` code style + + ```bash + pnpm format:check + ``` + +> We use `prettier-eslint` tool to override Prettier formatting for Vue components. + +### Linting + +To check whether ESLint rules pass, run: + +```bash +pnpm lint +``` + +To fix auto-fixable issues, run: + +```bash +pnpm lint --fix +``` + +### Testing + +We use [Vitest](https://vitest.dev/) test framework to assure quality of non-trivial internal parts of the project. + +To check whether tests pass, run: + +```bash +pnpm vitest run +``` + +To run vitest in a watch-mode, run: + +```bash +pnpm vitest +``` + +### Enabling feedback form + +In order to enable the "Share feedback" button, the following environment variable should be provided: + +```bash +VITE_FEEDBACK_URL=https://example.com/get-feedback +``` + +When a user submits the form, a simple POST request with a JSON body is sent to this URL. + +This variable will be picked up by the application during dev/build mode. Read more about it in the [Vite documentation](https://vitejs.dev/guide/env-and-mode.html). + +### Compatibility matrix + +**Note:** configuring this is **required**. + +The SDK Compatibility Matrix provides an insightful look into the interoperability of various stories across multiple SDKs within Hyperledger Iroha 2. + +The underlying data for the matrix is sourced from a [backend service](https://github.com/soramitsu/iroha2-docs-compat-matrix-service), ensuring low-latency response with preprocessed data. To configure access to the service (e.g. deployed at `https://docs-compat.iroha2.tachi.soramitsu.co.jp`), set the following environment variable: + +``` +VITE_COMPAT_MATRIX_URL=https://docs-compat.iroha2.tachi.soramitsu.co.jp/compat-matrix +``` + +## License + +Iroha documentation files are made available under the Creative Commons +Attribution 4.0 International License (CC-BY-4.0), available at +http://creativecommons.org/licenses/by/4.0/ diff --git a/etc/cli.ts b/etc/cli.ts new file mode 100644 index 000000000..c68b1757c --- /dev/null +++ b/etc/cli.ts @@ -0,0 +1,152 @@ +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' +import { SNIPPET_SRC_DIR } from './const' +import SOURCES from './snippet-sources' +import { SnippetSourceDefinition } from './types' +import { concurrentTasks, detectSaveCollisions, isAccessible, parseDefinition, ParsedSnippetDefinition } from './util' +import { match, P } from 'ts-pattern' +import fs from 'fs/promises' +import chalk from 'chalk' +import fetch from 'node-fetch' +import path from 'path' +import makeDir from 'make-dir' +import { deleteAsync } from 'del' +import { scanAndReport } from './validate-links' + +async function prepareOutputDir(options?: { clean?: boolean }) { + const optionClean = options?.clean ?? false + + const dirDisplay = chalk.bold(path.relative(process.cwd(), SNIPPET_SRC_DIR)) + + return Promise.resolve() + .then( + () => optionClean && deleteAsync([SNIPPET_SRC_DIR]).then(() => console.info(chalk`Deleted ${dirDisplay}`)), + ) + .then(() => makeDir(SNIPPET_SRC_DIR).then(() => console.info(chalk`Created ${dirDisplay}`))) +} + +async function processSnippet( + parsed: ParsedSnippetDefinition, + options?: { force?: boolean }, +): Promise<'skipped' | 'written'> { + const writePath = path.join(SNIPPET_SRC_DIR, parsed.saveFilename) + + if (!options?.force && (await isAccessible(writePath))) { + return 'skipped' + } + + const fileContent: string = await match>(parsed.source) + .with({ type: 'fs' }, async ({ path: snippetPath }) => { + return fs.readFile(snippetPath, { encoding: 'utf-8' }) + }) + .with({ type: 'hyper' }, async ({ url }) => { + return fetch(url).then((x) => { + if (x.ok) return x.text() + throw new Error(`Failed to fetch: ${x.status}`) + }) + }) + .exhaustive() + .then((content) => parsed.transform?.(content) ?? content) + + await fs.writeFile(writePath, fileContent) + + return 'written' +} + +yargs(hideBin(process.argv)) + .command( + 'get-snippets', + 'Parses snippet sources and collects them', + (y) => y.option('force', { type: 'boolean', default: true }), + async (opts) => { + const parseResult = SOURCES.map((x) => [x, parseDefinition(x)] as const).reduce<{ + ok: ParsedSnippetDefinition[] + err: { source: SnippetSourceDefinition; err: Error }[] + }>( + (acc, [item, result]) => { + match([result, acc] as const) + .with([{ type: 'error' }, P._], ([{ err }]) => acc.err.push({ source: item, err })) + .with([{ type: 'ok' }, P.when((x) => !x.err.length)], ([res]) => acc.ok.push(res)) + // eslint-disable-next-line @typescript-eslint/no-empty-function + .otherwise(() => {}) + + return acc + }, + { ok: [], err: [] }, + ) + + if (parseResult.err.length) { + for (const { source, err } of parseResult.err) { + console.error(chalk.red`Failed to parse snippet with source {bold ${source.src}}: ${String(err)}`) + } + throw new Error('Failed to parse sources') + } + + const { ok: parsed } = parseResult + + { + const collisions = detectSaveCollisions(parsed) + let thereAreCollisions = false + for (const [saveFile, sources] of collisions) { + thereAreCollisions = true + + const sourcesJoined = sources + .map((x) => + chalk.bold( + match(x) + .with({ type: 'fs' }, ({ path }) => `(fs) ${path}`) + .with({ type: 'hyper' }, ({ url }) => `(http) ${url}`) + .exhaustive(), + ), + ) + .join(', ') + console.error( + chalk.red`Multiple sources resolves into the same save file name {bold ${saveFile}}: ${sourcesJoined}`, + ) + } + if (thereAreCollisions) throw new Error('Collisions found') + } + + await prepareOutputDir({ clean: opts.force }) + + { + let done = 0 + const total = String(parsed.length) + const progressLabel = () => `[${String(done).padStart(total.length, ' ')}/${total}]` + + console.log('Fetching snippets...') + + await concurrentTasks(parsed, async (src) => { + try { + const result = await processSnippet(src, { force: opts.force }) + done++ + match(result) + .with('written', () => console.log(chalk.green`${progressLabel()} Written {bold ${src.saveFilename}}`)) + .with('skipped', () => console.log(chalk.gray`${progressLabel()} Skipped {bold ${src.saveFilename}}`)) + .exhaustive() + } catch (err) { + console.log(chalk.red`Failed to process {bold ${src.saveFilename}}`) + throw err + } + }) + + console.log('Done') + } + }, + ) + .command( + 'validate-links ', + 'Parses HTML output of VitePress and detects broken links', + (y) => + y + .positional('root', { description: "Root directory of VitePress's output", type: 'string', demandOption: true }) + .option('public-path', { description: 'Public path, used in the links', default: null, type: 'string' }), + async (opts) => { + await scanAndReport({ + root: opts.root, + publicPath: opts.publicPath ?? undefined, + }) + }, + ) + .showHelpOnFail(false) + .parse() diff --git a/etc/const.ts b/etc/const.ts new file mode 100644 index 000000000..afcf79fb6 --- /dev/null +++ b/etc/const.ts @@ -0,0 +1,3 @@ +import path from 'path' + +export const SNIPPET_SRC_DIR = path.resolve(__dirname, '../src/snippets') diff --git a/etc/meta.ts b/etc/meta.ts new file mode 100644 index 000000000..d34632004 --- /dev/null +++ b/etc/meta.ts @@ -0,0 +1,14 @@ +/** + * hyperledger-iroha/iroha#iroha2-dev + */ +export const IROHA_REV_DEV = 'e7a605c1a926c319d214ef3825524ee6c2e9f076' + +/** + * hyperledger-iroha/iroha-javascript#iroha2 (rc13) + */ +export const IROHA_JS_REV = '9c630fab14f063962b2508ac60e49789a160e443' + +/** + * hyperledger-iroha/iroha-java#iroha2-dev + */ +export const IROHA_JAVA_REV_DEV = 'e176225f935cc7f976d17384191ef0c0043ca0f6' diff --git a/etc/schema/index.ts b/etc/schema/index.ts new file mode 100644 index 000000000..5a3c950b2 --- /dev/null +++ b/etc/schema/index.ts @@ -0,0 +1 @@ +export { render } from './render' diff --git a/etc/schema/render.ts b/etc/schema/render.ts new file mode 100644 index 000000000..8cfb43d8c --- /dev/null +++ b/etc/schema/render.ts @@ -0,0 +1,191 @@ +import { Schema, SchemaTypeDefinition as Segment } from './types' +import { match, P } from 'ts-pattern' +// https://github.com/vuejs/vitepress/blob/b16340acbd3c60fee023daadb0ec5a0292060a1e/src/node/markdown/markdown.ts#L13 +import { slugify } from '@mdit-vue/shared' + +function segmentHeading(content: string) { + return `## ${code(content)}` +} + +function segmentPropNameOnly(prop: string) { + return `**${prop}:**` +} + +function segmentProp(name: string, content: string) { + return `${segmentPropNameOnly(name)} ${content}` +} + +function segmentType(type: string) { + return segmentProp('Type', type) +} + +function joinDouble(...lines: string[]): string { + return lines.join('\n\n') +} + +function tyMdLink(ty: string) { + return match(ty) + .with( + P.when((x: string): x is `u${number}` => /^u\d+$/.test(x)), + 'bool', + 'Bool', + 'String', + (x) => `${code(x)}`, + ) + .otherwise((ty) => `[${code(ty)}](#${slugify(ty)})`) +} + +function code(content: string) { + return `\`${content}\`` +} + +function table( + cols: (string | { title: string; align?: 'left' | 'right' | 'center' })[], + rows: string[][], + options: { indent: string }, +): string { + const { titles, aligns } = cols + .map<[string, string]>((x) => + typeof x === 'string' + ? [x, '---'] + : [ + x.title, + match(x.align) + .with('left', () => ':--') + .with('center', () => ':-:') + .with('right', () => '--:') + .with(undefined, () => '---') + .exhaustive(), + ], + ) + .reduce<{ titles: string[]; aligns: string[] }>( + (acc, [title, align]) => { + acc.titles.push(title) + acc.aligns.push(align) + return acc + }, + { titles: [], aligns: [] }, + ) + + return [titles, aligns, ...rows].map((row) => `${options.indent}| ${row.join(' | ')} |`).join('\n') +} + +function predicateNotFalse(value: false | T): value is T { + return Boolean(value) +} + +function renderSegment(segment: Segment, segmentName: string): string { + const heading = segmentHeading(segmentName) + + const body = match(segment) + .with({ Struct: P.select() }, (declarations) => + joinDouble( + segmentType('Struct'), + segmentPropNameOnly('Declarations'), + table( + [ + { title: 'Field name', align: 'right' }, + { title: 'Field value', align: 'left' }, + ], + declarations.map((x) => [code(x.name), tyMdLink(x.type)]), + { indent: ' ' }, + ), + ), + ) + .with({ Enum: P.select() }, (variants) => + joinDouble( + segmentType('Enum'), + segmentPropNameOnly('Variants'), + table( + [{ title: 'Variant name', align: 'right' }, { title: 'Variant value', align: 'left' }, 'Discriminant'], + variants.map((x) => [code(x.tag), x.type ? tyMdLink(x.type) : `—`, String(x.discriminant)]), + { indent: ' ' }, + ), + ), + ) + .with({ Tuple: P.select() }, (types) => + joinDouble( + // . + segmentType('Tuple'), + segmentProp('Values', `(` + types.map((ty) => tyMdLink(ty)).join(', ') + `)`), + ), + ) + .with({ Map: P.select() }, ({ key, value }) => + joinDouble( + ...[ + segmentType('Map'), + segmentProp('Key', tyMdLink(key)), + segmentProp('Value', tyMdLink(value)), + // sorted_by_key && segmentProp('Sorted by key', 'Yes'), + ].filter(predicateNotFalse), + ), + ) + .with({ Vec: P.select() }, (ty) => + joinDouble( + ...[ + // + segmentType('Vec'), + segmentProp('Value', tyMdLink(ty)), + // sorted && segmentProp('Sorted', 'Yes'), + ].filter(predicateNotFalse), + ), + ) + .with({ Int: P.select() }, (str) => joinDouble(segmentType('Int'), segmentProp('Kind', str))) + .with({ FixedPoint: P.select() }, ({ base, decimal_places }) => + joinDouble( + segmentType('Fixed Point'), + segmentProp('Base', code(base)), + segmentProp('Decimal places', String(decimal_places)), + ), + ) + .with({ Array: P.select() }, ({ type, len }) => + joinDouble( + // . + segmentType('Array'), + segmentProp('Length', String(len)), + segmentProp('Value', tyMdLink(type)), + ), + ) + .with({ Option: P.select() }, (ty) => + joinDouble( + // . + segmentType('Option'), + segmentProp('Some', tyMdLink(ty)), + ), + ) + .with(P.string, (alias) => + joinDouble( + // . + segmentType('Alias'), + segmentProp('To', tyMdLink(alias)), + ), + ) + .with(null, () => segmentType('Zero-Size Type (unit type, null type)')) + .with({ Bitmap: P.select() }, ({ repr, masks }) => + joinDouble( + // . + segmentType('Bitmap'), + segmentProp('Repr', repr), + segmentPropNameOnly('Masks'), + table( + [ + { title: 'Field name', align: 'right' }, + { title: 'Field value', align: 'left' }, + ], + masks.map((x) => [code(x.name), code('0x' + x.mask.toString(16))]), + { indent: ' ' }, + ), + ), + ) + .exhaustive() + + return joinDouble(heading, body) +} + +/** + * Returns Markdown + */ +export function render(schema: Schema): string { + const entries = Object.entries(schema) + return joinDouble(...entries.map(([name, segment]) => renderSegment(segment, name))) +} diff --git a/etc/schema/types.ts b/etc/schema/types.ts new file mode 100644 index 000000000..92e8fd294 --- /dev/null +++ b/etc/schema/types.ts @@ -0,0 +1,92 @@ +// FIXME: this file reflects types from `@iroha2/data-model-schema` package which isn't published yet +// https://github.com/hyperledger-iroha/iroha-javascript/pull/170 + +export interface Schema { + [type: string]: SchemaTypeDefinition +} + +export type SchemaTypeDefinition = + | UnitType + | DirectAlias + | MapDefinition + | VecDefinition + | OptionDefinition + | NamedStructDefinition + | EnumDefinition + | ArrayDefinition + | IntDefinition + | FixedPointDefinition + | TupleDef + | BitmapDef + +export interface MapDefinition { + Map: { + key: TypePath + value: TypePath + } +} + +export interface TupleDef { + Tuple: TypePath[] +} + +export type DirectAlias = TypePath + +export interface VecDefinition { + Vec: TypePath +} + +export interface ArrayDefinition { + Array: { + len: number + type: TypePath + } +} + +export interface OptionDefinition { + Option: TypePath +} + +export interface NamedStructDefinition { + Struct: Array<{ + name: string + type: TypePath + }> +} + +export interface EnumDefinition { + Enum: Array +} + +export interface EnumVariantDefinition { + tag: string + discriminant: number + type?: TypePath +} + +export interface IntDefinition { + Int: string +} + +export interface FixedPointDefinition { + FixedPoint: { + base: string + decimal_places: number + } +} + +export type TypePath = string + +export type UnitType = null + +export interface BitmapMask { + name: string + mask: number +} + +export interface BitmapDef { + Bitmap: { + repr: string + masks: Array + } +} diff --git a/etc/snippet-sources.ts b/etc/snippet-sources.ts new file mode 100644 index 000000000..bb4b14592 --- /dev/null +++ b/etc/snippet-sources.ts @@ -0,0 +1,146 @@ +import type { SnippetSourceDefinition } from './types' +import { IROHA_JAVA_REV_DEV, IROHA_JS_REV, IROHA_REV_DEV } from './meta' +import { render as renderDataModelSchema } from './schema' + +// ***** + +const javascriptSnippets = [ + { + src: 'packages/docs-recipes/src/1.client-install.ts', + local: '1-client-install.ts', + }, + { + src: 'packages/docs-recipes/src/2.1.1.key-pair.ts', + local: '2-1-1-key-pair.ts', + }, + { + src: 'packages/docs-recipes/src/2.1.2.signer.ts', + local: '2-1-2-signer.ts', + }, + { + src: 'packages/docs-recipes/src/2.2.1.torii-usage-example.ts', + local: '2-2-1-torii-usage-example.ts', + }, + { + src: 'packages/docs-recipes/src/2.2.2.torii-pre-node.ts', + local: '2-2-2-torii-pre-node.ts', + }, + { + src: 'packages/docs-recipes/src/2.2.3.torii-pre-web.ts', + local: '2-2-3-torii-pre-web.ts', + }, + { + src: 'packages/docs-recipes/src/2.3.client.ts', + local: '2-3-client.ts', + }, + { + src: 'packages/docs-recipes/src/3.register-domain.ts', + local: '3-register-domain.ts', + }, + { + src: 'packages/docs-recipes/src/4.register-account.ts', + local: '4-register-account.ts', + }, + { + src: 'packages/docs-recipes/src/5.1.register-asset.ts', + local: '5-1-register-asset.ts', + }, + { + src: 'packages/docs-recipes/src/5.2.mint-registered-asset.ts', + local: '5-2-mint-asset.ts', + }, + { + src: 'packages/docs-recipes/src/6.transfer-assets.ts', + local: '6-transfer-assets.ts', + }, + { + src: 'packages/docs-recipes/src/7.query-domains-accounts-assets.ts', + local: '7-querying.ts', + }, + { + src: 'packages/client/test/integration/test-web/src/main.ts', + local: '8-main.ts', + }, + { + src: 'packages/client/test/integration/config/client_config.json', + local: '8-config.json', + }, + { + src: 'packages/client/test/integration/test-web/src/App.vue', + local: '8-App.vue', + }, + { + src: 'packages/client/test/integration/test-web/src/client.ts', + local: '8-client.ts', + }, + { + src: 'packages/client/test/integration/test-web/src/crypto.ts', + local: '8-crypto.ts', + }, + { + src: 'packages/client/test/integration/test-web/src/components/CreateDomain.vue', + local: '8-components-CreateDomain.vue', + }, + { + src: 'packages/client/test/integration/test-web/src/components/StatusChecker.vue', + local: '8-components-StatusChecker.vue', + }, + { + src: 'packages/client/test/integration/test-web/src/components/EventListener.vue', + local: '8-components-EventListener.vue', + }, + { + src: 'packages/docs-recipes/src/9.blocks-stream.ts', + local: '9-blocks-stream.ts', + }, +].map(({ src, local }) => ({ + src: `https://raw.githubusercontent.com/hyperledger-iroha/iroha-javascript/${IROHA_JS_REV}/${src}`, + filename: `js-sdk-${local}`, +})) + +// ***** + +export default [ + { + src: `https://raw.githubusercontent.com/hyperledger-iroha/iroha/${IROHA_REV_DEV}/MAINTAINERS.md`, + filename: 'iroha-maintainers.md', + }, + { + src: `https://raw.githubusercontent.com/hyperledger-iroha/iroha/${IROHA_REV_DEV}/docs/source/references/schema.json`, + filename: `data-model-schema.md`, + transform: (source) => { + return renderDataModelSchema(JSON.parse(source)) + }, + }, + { + src: './src/example_code/lorem.rs', + }, + { + src: `https://raw.githubusercontent.com/hyperledger-iroha/iroha/${IROHA_REV_DEV}/configs/client.template.toml`, + filename: 'client-cli-config-template.toml', + }, + { + src: `https://raw.githubusercontent.com/hyperledger-iroha/iroha/${IROHA_REV_DEV}/configs/peer.template.toml`, + filename: 'peer-config-template.toml', + }, + { + src: `https://raw.githubusercontent.com/hyperledger-iroha/iroha/${IROHA_REV_DEV}/configs/swarm/genesis.json`, + }, + { + src: `https://raw.githubusercontent.com/hyperledger-iroha/iroha/${IROHA_REV_DEV}/client/examples/tutorial.rs`, + filename: 'tutorial-snippets.rs', + }, + + ...javascriptSnippets, + + { + src: `https://raw.githubusercontent.com/hyperledger-iroha/iroha-java/${IROHA_JAVA_REV_DEV}/modules/test-tools/src/main/kotlin/jp/co/soramitsu/iroha2/testengine/IrohaConfig.kt`, + filename: 'IrohaConfig.kotlin', + }, + { + src: `https://raw.githubusercontent.com/hyperledger-iroha/iroha-java/${IROHA_JAVA_REV_DEV}/modules/client/src/test/kotlin/jp/co/soramitsu/iroha2/InstructionsTest.kt`, + }, + { + src: `https://raw.githubusercontent.com/hyperledger-iroha/iroha-java/${IROHA_JAVA_REV_DEV}/modules/client/src/test/java/jp/co/soramitsu/iroha2/JavaTest.java`, + }, +] satisfies SnippetSourceDefinition[] diff --git a/etc/types.ts b/etc/types.ts new file mode 100644 index 000000000..4e117290e --- /dev/null +++ b/etc/types.ts @@ -0,0 +1,19 @@ +export interface SnippetSourceDefinition { + /** + * URI from which to get the snippet. + * + * Could be: + * + * - HTTP(S) URL (e. g. `http://example.com/hack.rs`) + * - File system path (e. g. `./file.ts`, `src/file.ts`) + */ + src: string + /** + * (Optional) The name that the source file will have in the snippets directory. + */ + filename?: string + /** + * **Advanced:** transform loaded content before writing it in the snippets directory + */ + transform?: (content: string) => string | Promise +} diff --git a/etc/util.spec.ts b/etc/util.spec.ts new file mode 100644 index 000000000..314bb5fc2 --- /dev/null +++ b/etc/util.spec.ts @@ -0,0 +1,45 @@ +import { test, expect, describe } from 'vitest' +import { SnippetSourceDefinition } from './types' +import { ParseDefinitionResult, parseSnippetSrc, parseDefinition, rewriteMdLinks } from './util' + +describe('Parse snippet src', () => { + test.each([ + ['./src/snippet.ts', { type: 'fs', path: './src/snippet.ts' }], + ['http://github.com', { type: 'hyper', url: 'http://github.com' }], + ['/abs', { type: 'fs', path: '/abs' }], + ])('Parses %o to %o', (input, result) => { + expect(parseSnippetSrc(input)).toEqual(result) + }) +}) + +describe('Parse snippet source definition', () => { + test.each([ + [{ src: './hey' }, { type: 'ok', saveFilename: 'hey', source: { type: 'fs', path: './hey' }, transform: null }], + [ + { src: 'https://github.com/file.ts', filename: 'override.ts' }, + { + type: 'ok', + source: { type: 'hyper', url: 'https://github.com/file.ts' }, + saveFilename: 'override.ts', + transform: null, + }, + ], + ] satisfies [SnippetSourceDefinition, ParseDefinitionResult][])('Parses %o to %o', (input, output) => { + expect(parseDefinition(input)).toEqual(output) + }) +}) + +describe('links rewrite', () => { + const BASE = 'https://github.com/a/b/c/d/e/f' + + test('./config.md', () => { + expect(rewriteMdLinks(BASE)(`[cfg](./config.md)`)).toMatchInlineSnapshot( + '"[cfg](https://github.com/a/b/c/d/e/f/config.md)"', + ) + }) + test('../../../client#foo', () => { + expect(rewriteMdLinks(BASE)(`[foo](../../../client#foo)`)).toMatchInlineSnapshot( + '"[foo](https://github.com/a/b/c/client#foo)"', + ) + }) +}) diff --git a/etc/util.ts b/etc/util.ts new file mode 100644 index 000000000..92c1f86b5 --- /dev/null +++ b/etc/util.ts @@ -0,0 +1,122 @@ +import { SnippetSourceDefinition } from './types' +import { match, P } from 'ts-pattern' +import path from 'path' +import { URL } from 'url' +import fs from 'fs/promises' + +export async function isAccessible(path: string): Promise { + return fs + .access(path) + .then(() => true) + .catch(() => false) +} + +export function parseSnippetSrc(src: string): ParsedSource { + if (/^http(s)?:\/\//.test(src)) return { type: 'hyper', url: src } + return { type: 'fs', path: src } +} + +type ParsedSource = { type: 'fs'; path: string } | { type: 'hyper'; url: string } + +export interface ParsedSnippetDefinition { + source: ParsedSource + saveFilename: string + transform: null | ((content: string) => Promise) +} + +export type ParseDefinitionResult = ({ type: 'ok' } & ParsedSnippetDefinition) | { type: 'error'; err: Error } + +export function parseDefinition(definition: SnippetSourceDefinition): ParseDefinitionResult { + return match(parseSnippetSrc(definition.src)) + .with({ type: P.union('fs', 'hyper') }, (source): ParseDefinitionResult => { + let saveFilename: string + + if (definition.filename) { + saveFilename = definition.filename + } else { + const uri = match(source) + .with({ type: 'fs' }, (x) => x.path) + .with({ type: 'hyper' }, (x) => x.url) + .exhaustive() + + saveFilename = path.basename(uri) + } + + const { transform } = definition + + return { + type: 'ok', + source, + saveFilename, + transform: transform ? async (x) => transform(x) : null, + } + }) + .exhaustive() +} + +export function detectSaveCollisions( + parsedDefinitions: ParsedSnippetDefinition[], +): Map { + const map = new Map() + + for (const i of parsedDefinitions) { + const items = map.get(i.saveFilename) ?? [] + items.push(i.source) + map.set(i.saveFilename, items) + } + + return new Map( + [...map].filter((x): x is [string, [ParsedSource, ParsedSource, ...ParsedSource[]]] => x[1].length > 1), + ) +} + +export function concurrentTasks(data: T[], fn: (data: T) => Promise, maxConcurrency = 20): Promise { + let cursor = 0 + let wip = 0 + + let resolve: () => void + let reject: (reason?: unknown) => void + const promise = new Promise((promResolve, promReject) => { + resolve = promResolve + reject = promReject + }) + + function handleResolve() { + wip-- + if (!wip && cursor === data.length) resolve() + else executeTasks() + } + + function handleReject(reason?: unknown) { + reject(reason) + } + + function executeTasks() { + while (wip < maxConcurrency && cursor < data.length) { + const dataItem = data[cursor++] + fn(dataItem).then(handleResolve).catch(handleReject) + wip++ + } + } + + executeTasks() + return promise +} + +/** + * Replace relative to some URL Markdown links with actual links to that URL + * + * Examples: + * + * - `(./config.md#some_hash)` -> `(/config.md#some_hash)` + * - `(../../client)` -> `(/client)` + * + * FIXME: unused + */ +export function rewriteMdLinks(base: string): (markdown: string) => string { + return (markdown) => + markdown.replaceAll(/\((\..+?)([)#])/g, (_sub, relative, ending) => { + const rewritten = new URL(path.join(base, relative)).href + return `(${rewritten}${ending}` + }) +} diff --git a/etc/validate-links.spec.ts b/etc/validate-links.spec.ts new file mode 100644 index 000000000..1068dc605 --- /dev/null +++ b/etc/validate-links.spec.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from 'vitest' +import { LinkOtherFile, LinkSelfAnchor, parseLink } from './validate-links' + +describe('Parse link', () => { + test('Parse self link', () => { + const result = parseLink({ + root: '.', + source: './a/b.html', + href: '#afse', + }) + + expect(result).toEqual({ type: 'self', anchor: 'afse' } satisfies LinkSelfAnchor) + }) + + test('Parse link with public path', () => { + const result = parseLink({ + root: '/root', + source: '/root/foo/bar.html', + href: '/pub/baz.html', + publicPath: '/pub/', + }) + + expect(result).toEqual({ + type: 'other', + file: '/root/baz.html', + } satisfies LinkOtherFile) + }) + + test('Fallback to index.html when public path is specified', () => { + const result = parseLink({ + root: '/root', + source: '/root/foo/bar.html', + href: '/pub/#zzz', + publicPath: '/pub/', + }) + + expect(result).toEqual({ + type: 'other', + file: '/root/index.html', + anchor: 'zzz', + } satisfies LinkOtherFile) + }) + + test('Fallback to index.html without public path', () => { + const result = parseLink({ + root: '/root', + source: '/root/foo/bar.html', + href: '/#zzz', + }) + + expect(result).toEqual({ + type: 'other', + file: '/root/index.html', + anchor: 'zzz', + } satisfies LinkOtherFile) + }) +}) diff --git a/etc/validate-links.ts b/etc/validate-links.ts new file mode 100644 index 000000000..b69290513 --- /dev/null +++ b/etc/validate-links.ts @@ -0,0 +1,306 @@ +import { readFile } from 'fs/promises' +import chalk from 'chalk' +import { P, match } from 'ts-pattern' +import { globby } from 'globby' +import path from 'path' +import * as htmlparser from 'htmlparser2' +import * as cssSelect from 'css-select' +import leven from 'leven' +import fastDiff from 'fast-diff' + +interface Options { + root: string + publicPath?: string +} + +type LinkIssues = Map + +type LinkIssue = IssueMissingOtherFile | IssueMissingAnchorInOtherFile | IssueMissingAnchorInSelf + +interface IssueMissingOtherFile { + type: 'missing-other-file' + file: string +} + +interface IssueMissingAnchorInOtherFile { + type: 'missing-id-in-other' + file: string + id: string + similar?: string[] +} + +interface IssueMissingAnchorInSelf { + type: 'missing-id-in-self' + id: string + similar?: string[] +} + +export async function scanAndReport(options: Options) { + const issues = await scan(options) + + const count = countIssues(issues) + if (count === 0) { + console.log(chalk`{green ✓ Haven't detected any broken links}`) + } else { + const sortByFileName = (items: T[]): T[] => { + const arr = [...items] + arr.sort(([a], [b]) => { + return a < b ? -1 : a > b ? 1 : 0 + }) + return arr + } + + const formatFile = (file: string): string => { + const relative = path.relative(options.root, file) + const ext = path.extname(relative) + return chalk`${relative.slice(0, -ext.length)}{reset.dim ${ext}}` + } + + const formatSimilar = (origin: string, similar?: string[]) => { + if (!similar?.length) return '' + + // format diffs + const diffs = similar.map((x) => { + const diff = fastDiff(origin, x) + .map(([kind, piece]) => { + return match(kind) + .with(fastDiff.EQUAL, () => chalk.gray.dim(piece)) + .with(fastDiff.INSERT, () => chalk.bgGreenBright.black(piece)) + .with(fastDiff.DELETE, () => chalk.bgRedBright.black(piece)) + .exhaustive() + }) + .join('') + + return chalk`{blue ${x}} {gray.dim (diff: ${diff}})` + }) + + return `. Here are similar ones:\n ` + diffs.join('\n ') + } + + const formattedIssues = sortByFileName([...issues]) + .map(([file, issues]) => { + const issuesFormatted: string = issues + .map((issue) => { + return ( + match(issue) + // actually, it should never happen: VitePress disallows dead links to other pages + .with({ type: 'missing-other-file' }, (x) => { + return ( + chalk` Broken link: {bold.red ${formatFile(x.file)}}\n ` + chalk`{red Cannot find the file.}` + ) + }) + .with({ type: 'missing-id-in-other' }, (x) => { + return ( + chalk` Broken link: {bold ${formatFile(x.file)}{red #${x.id}}}` + + `\n ` + + chalk`{red Cannot find the ID in the other file}` + + formatSimilar(x.id, x.similar) + ) + }) + .with({ type: 'missing-id-in-self' }, (x) => { + return ( + chalk` Broken link: {bold.red #${x.id}}\n ` + + chalk`{red Cannot find the ID within the file itself}` + + formatSimilar(x.id, x.similar) + ) + }) + .exhaustive() + ) + }) + .join('\n\n') + + return `${chalk.underline.bold(formatFile(file))} (issues: ${issues.length})\n${issuesFormatted}` + }) + .join('\n\n') + + console.error( + `${formattedIssues}\n\n${chalk.red(`× Found broken links. Total issues count: ${chalk.bold(count)}`)}`, + ) + process.exit(1) + } +} + +function countIssues(issues: LinkIssues): number { + let count = 0 + for (const x of issues.values()) { + count += x.length + } + return count +} + +async function scan(options: Options): Promise { + const files = await findFiles(options.root) + + const entries = await Promise.all( + files.map(async (file) => { + const html = await readFile(file, { encoding: 'utf-8' }) + const { links, anchors } = scanLinksAndAnchorsInHTML(html) + + const parsedLinks = links + .map((x) => parseLink({ root: options.root, source: file, href: x, publicPath: options.publicPath })) + .filter((x): x is Exclude => x.type !== 'external') + + return { file, links: parsedLinks, anchors } + }), + ) + + const graph = entries.reduce((acc, { file, links, anchors }) => { + acc.set(file, { links, anchors }) + return acc + }, new Map()) + + return findIssuesInGraph(graph) +} + +async function findFiles(root: string): Promise { + return globby(path.join(root, '**/*.html')) +} + +const ANCHORS_QUERY = cssSelect.compile('main [id]') + +const LINKS_QUERY = cssSelect.compile('main a[href]') + +/** + * TODO: Here we only look into ``. There are also links in `
+ Unable to render the diagram +
{{ taskState.rejected.reason }}
{{ textDecoded }}