diff --git a/content/@theme/markdoc/components.tsx b/content/@theme/markdoc/components.tsx index c0479ae0ea..471e0d0c55 100644 --- a/content/@theme/markdoc/components.tsx +++ b/content/@theme/markdoc/components.tsx @@ -4,19 +4,59 @@ import dynamicReact from '@markdoc/markdoc/dist/react'; import { usePageSharedData } from '@portal/hooks'; import { Link } from '@portal/Link'; +function slugify(text: string) { + return text + .toLowerCase() + .replace(/ /g, '-') + .replace(/[^\w-]+/g, ''); +} + export function IndexPageItems() { const data = usePageSharedData('index-page-items') as any[]; return (
+ ); +} + +export function StartStep(props: { children: React.ReactNode; stepIdx: number; steps: string[] }) { + const stepLabel = props.steps[props.stepIdx]; + const stepId = slugify(stepLabel); + + return ( +
+
+
+
    + {props.steps.map((step, idx) => { + const iterStepId = slugify(step).toLowerCase(); + let className = `breadcrumb-item bc-${iterStepId}`; + if (idx > 0) className += ' disabled'; + if (iterStepId === stepId) className += ' current'; + return ( +
  • + {step} +
  • + ); + })} +
+
+
{dynamicReact(props.children, React, {})}
+
+
); } diff --git a/content/@theme/plugin.js b/content/@theme/plugin.js index 2c7f8c1f6b..3f71769b2a 100644 --- a/content/@theme/plugin.js +++ b/content/@theme/plugin.js @@ -1,63 +1,23 @@ -// TODO: export function from root package -import { readSharedData } from '@redocly/portal/dist/server/utils/shared-data.js'; +import { indexPages } from './plugins/index-pages.js'; +import { codeSamples } from './plugins/code-samples.js'; + +export default function customPlugin() { + const indexPagesInst = indexPages(); + const codeSamplesInst = codeSamples(); + -const INDEX_PAGE_INFO_DATA_KEY = 'index-page-items'; -export default function indexPagesPlugin() { /** @type {import("@redocly/portal/dist/server/plugins/types").PluginInstance } */ const pluginInstance = { - // hook that gets executed after all routes were created - async afterRoutesCreated(contentProvider, actions) { - // get all the routes that are ind pages - const indexRoutes = actions.getAllRoutes().filter(route => route.metadata?.indexPage); - - for (const route of indexRoutes) { - // this uses some internals, we will expose them in nicer way in the future releases - const sidebarId = actions.routesSharedData.get(route.slug)?.['sidebar']; // TODO: implement a helper function for this - const sidebar = await readSharedData(sidebarId, actions.outdir); - - if (!sidebar) { - console.log('Index route used with no sidebar', route.fsPath); - continue; - } - - const item = findItemDeep(sidebar.items, route.fsPath); - const childrenPaths =(item.items || []).map(item => item.fsPath).filter(Boolean); - - const childRoutes = childrenPaths.map(fsPath => actions.getRouteByFsPath(fsPath)); - const childRoutesData = await Promise.all( - childRoutes.map(async route => { - const { - parsed: { data }, - } = contentProvider.loadContent(route.fsPath, 'frontmatter'); - return { - ...data, - slug: route.slug, - title: await route.getNavText(), - }; - }) - ); - - const sharedDataId = await actions.createSharedData(route.slug + '_' + INDEX_PAGE_INFO_DATA_KEY, childRoutesData); - actions.addRouteSharedData(route.slug, INDEX_PAGE_INFO_DATA_KEY, sharedDataId) - } + processContent: async (content, actions) => { + await indexPagesInst.processContent?.(content, actions); + await codeSamplesInst.processContent?.(content, actions); + }, + afterRoutesCreated: async (content, actions) => { + await indexPagesInst.afterRoutesCreated?.(content, actions); + await codeSamplesInst.afterRoutesCreated?.(content, actions); }, }; return pluginInstance; } - -function findItemDeep(items, fsPath) { - for (const item of items) { - if (item.fsPath === fsPath) { - return item; - } - - if (item.items) { - const found = findItemDeep(item.items, fsPath); - if (found) { - return found; - } - } - } -} diff --git a/content/@theme/plugins/code-samples.js b/content/@theme/plugins/code-samples.js new file mode 100644 index 0000000000..86d2a94518 --- /dev/null +++ b/content/@theme/plugins/code-samples.js @@ -0,0 +1,112 @@ +// @ts-check + +import { getInnerText } from '@redocly/portal/dist/shared/markdoc.js'; + +import { dirname, relative, join as joinPath } from 'path'; +import markdoc from '@markdoc/markdoc'; + +export function codeSamples() { + /** @type {import("@redocly/portal/dist/server/plugins/types").PluginInstance } */ + const instance = { + processContent: async (contentProvider, actions) => { + try { + const samples = []; + const allLands = new Set(); + const allCodeSampleFiles = Array.from(contentProvider.fsFilesList.values()); + + const readmes = allCodeSampleFiles.filter(file => file.match(/_code-samples[\/\\]([^\\\/]*)[\/\\]README\.md$/)); + + for (const relativePath of readmes) { + const record = contentProvider.loadContent(relativePath, 'frontmatter'); + + const ast = markdoc.parse(record.content); + + const dirPath = dirname(relativePath) + const langs = unique( + allCodeSampleFiles + .filter(file => file.startsWith(dirPath) && !file.endsWith('README.md')) + .map(file => relative(dirPath, file).split('/')[0]) + ); + const title = extractFirstHeading(ast) || ''; + samples.push({ + path: dirPath, + title: title || toTitleCase(dirname(dirPath)), + description: getInnerText([ast.children[1]]).replace(title, '').trim(), + href: joinPath('content', dirPath), + langs, + }); + + langs.forEach(l => allLands.add(l)); + } + + const sortedSamples = samples.sort((a, b) => normalizeTitleForSort(a).localeCompare(normalizeTitleForSort(b))); + + actions.createSharedData('code-samples', { codeSamples: sortedSamples, langs: Array.from(allLands) }); + actions.addRouteSharedData('/code-samples/', 'code-samples', 'code-samples'); + } catch (e) { + console.log(e); + } + }, + }; + return instance; +} + +function normalizeTitleForSort(cs) { + if (cs.title.includes('Intro') || cs.title.includes('Quickstart')) { + return ` ${cs.title}`; + } + return cs.title; +} + +const WORDS_TO_CAPS = ['xrp']; + +function toTitleCase(s) { + const words = s.split(/_|[^\w']/); + return words + .filter(word => word) + .map(word => (WORDS_TO_CAPS.includes(word) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1))) + .join(' ') + .replace("'S", "'s") + .replace(' A ', ' a '); +} + +function unique(array) { + return Array.from(new Set(array)); +} + +function extractFirstHeading(ast) { + let heading; + + visit(ast, node => { + if (!isNode(node)) { + return; + } + + if (node.type === 'heading') { + heading = getInnerText([node]); + return EXIT; + } + }); + + return heading; +} + +function isNode(value) { + return !!(value?.$$mdtype === 'Node'); +} + +const EXIT = Symbol('Exit visitor'); +function visit(node, visitor) { + if (!node) return; + + const res = visitor(node); + if (res === EXIT) return res; + + for (const child of node.children) { + if (!child || typeof child === 'string') { + continue; + } + const res = visit(child, visitor); + if (res === EXIT) return res; + } +} diff --git a/content/code-samples.page.tsx b/content/code-samples.page.tsx new file mode 100644 index 0000000000..bb06a08cd3 --- /dev/null +++ b/content/code-samples.page.tsx @@ -0,0 +1,115 @@ +import * as React from 'react'; +import { usePageSharedData, useTranslate } from '@portal/hooks'; + +const langIcons = { + cli: require('./static/img/logos/cli.svg'), + go: require('./static/img/logos/golang.svg'), + java: require('./static/img/logos/java.svg'), + js: require('./static/img/logos/javascript.svg'), + py: require('./static/img/logos/python.svg'), + http: require('./static/img/logos/globe.svg'), +}; +const target = { + github_forkurl: 'https://github.com/XRPLF/xrpl-dev-portal', + github_branch: 'master', +}; + +export default function CodeSamples() { + const { translate } = useTranslate(); + const { codeSamples, langs } = usePageSharedData('code-samples'); + + return ( +
+
+
+
+
+

{translate('Start Building with Example Code')}

+
{translate('Code Samples')}
+
+ Submit Code Samples +
+
+
+ default-alt-text +
+
+
+

+ {translate('Browse sample code for building common use cases on the XRP Ledger')} +

+
+ +
+
+
+
+

{translate('Contribute Code Samples')}

+
+ {translate('Help the XRPL community by submitting your
own code samples')} +
+
+
+
+ +
Fork and clone
+

+ Fork the xrpl-dev-portal repo. Using git, clone + the fork to your computer. +

+
+
+ +
Add to folder
+

+ Add your sample code to the content/_code-samples/ folder. Be sure to include a{' '} + README.md that summarizes what it does and anything else people should know about it. +

+
+
+ +
Commit and push
+

Commit your changes and push them to your fork on GitHub.

+
+
+ +
Open a pull request
+

+ Open a pull request to the original repo. Maintainers will review your submission and suggest changes + if necessary. If the code sample is helpful, it'll be merged and added to XRPL.org! +

+
+
+ Submit Code Samples +
+
+
+
+ ); +} diff --git a/content/docs.page.tsx b/content/docs.page.tsx index e2fe0fddd1..39cc7deee3 100644 --- a/content/docs.page.tsx +++ b/content/docs.page.tsx @@ -388,7 +388,7 @@ export default function Docs() {

{translate('Browse By Recommended Pages')}