diff --git a/docs/src/components/workshop/WorkshopExperience.astro b/docs/src/components/workshop/WorkshopExperience.astro index 42375d4e543..f7c578b30d1 100644 --- a/docs/src/components/workshop/WorkshopExperience.astro +++ b/docs/src/components/workshop/WorkshopExperience.astro @@ -1,5 +1,5 @@ --- -import { Card, CardGrid } from '@astrojs/starlight/components'; +import { CardGrid, LinkCard } from '@astrojs/starlight/components'; // @ts-ignore The docs app already uses this runtime package from Astro components. import octicons from '@primer/octicons'; import { @@ -87,10 +87,6 @@ function decodeLocalWorkshopStepLink(value: string) { return isLocalWorkshopStepLink(value) ? value : ''; } -function firstSentence(value: string) { - return value.match(/^.*?[.!?](?:\s|$)/u)?.[0].trim() ?? value.trim(); -} - function sanitizeWorkshopHtml(html: string) { return html .replace(/]*>[\s\S]*?<\/script>/giu, '') @@ -101,53 +97,16 @@ function sanitizeWorkshopHtml(html: string) { function transformTables(html: string) { return html.replace(/([\s\S]*?)<\/table>/gu, (tableHtml) => { - // Check if this is the entry path table by inspecting the cells. - const theadMatch = tableHtml.match(/([\s\S]*?)<\/thead>/u); - const thMatches = theadMatch ? [...theadMatch[1].matchAll(/]*)?>([\s\S]*?)<\/th>/gu)] : []; - const normalizeHeader = (value: string) => value - .replace(/<[^>]+>/gu, '') - .replace(/[‘’]/gu, "'") - .replace(/…/gu, '...') - .replace(/\s+/gu, ' ') - .trim(); - - if ( - thMatches.length >= 3 - && normalizeHeader(thMatches[0][1]) === "If you're a..." - && normalizeHeader(thMatches[1][1]) === 'Recommended setup adventure' - && normalizeHeader(thMatches[2][1]) === 'Why this path fits' - ) { - const tbodyMatch = tableHtml.match(/([\s\S]*?)<\/tbody>/u); - const rows: [string, string, string][] = []; - if (tbodyMatch) { - for (const rowMatch of tbodyMatch[1].matchAll(/([\s\S]*?)<\/tr>/gu)) { - const tdMatches = [...rowMatch[1].matchAll(/]*)?>([\s\S]*?)<\/td>/gu)]; - if (tdMatches.length >= 3) { - rows.push([tdMatches[0][1].trim(), tdMatches[1][1].trim(), tdMatches[2][1].trim()]); - } - } - } - - if (rows.length > 0) { - const cards = rows.map(([audience, nextStep, fit]) => [ - '
', - '

Entry path

', - `

${audience}

`, - `

${fit}

`, - '
', - 'Recommended next step', - `

${nextStep}

`, - '
', - '
', - ].join('')); - return `
${cards.join('')}
`; - } - } - return `
${tableHtml}
`; }); } +function renderWorkshopAlert(type: string, inner: string) { + const cls = `aw-workshop-admonition-${type.toLowerCase()}`; + const title = type.charAt(0).toUpperCase() + type.slice(1).toLowerCase(); + return ``; +} + function rewriteGfmAlerts(html: string) { // Pattern to detect a blockquote that opens with a GFM alert type marker. const openingPattern = /
\s*

\[!(NOTE|TIP|WARNING|IMPORTANT|CAUTION)\]/giu; @@ -193,8 +152,6 @@ function rewriteGfmAlerts(html: string) { const blockquoteEnd = foundClose + '

'.length; const afterType = html.slice(afterTypeStart, foundClose); - const cls = `aw-workshop-admonition-${type.toLowerCase()}`; - const title = type.charAt(0).toUpperCase() + type.slice(1).toLowerCase(); let inner: string; if (afterType.startsWith('

')) { // [!TYPE]

rest...

— type was alone in first paragraph @@ -207,7 +164,7 @@ function rewriteGfmAlerts(html: string) { } result += html.slice(lastIndex, matchStart); - result += ``; + result += renderWorkshopAlert(type, inner); lastIndex = blockquoteEnd; // Skip the regex past the entire consumed blockquote so it does not // re-match patterns that appear inside the body we just replaced. @@ -215,7 +172,12 @@ function rewriteGfmAlerts(html: string) { } result += html.slice(lastIndex); - return result; + // Some workshop content emits a standalone alert paragraph instead of a full + // blockquote wrapper, so normalize those markers into the same aside UI too. + return result.replace( + /

\s*\[!(NOTE|TIP|WARNING|IMPORTANT|CAUTION)\]\s*([\s\S]*?)<\/p>/giu, + (_match, type, body) => renderWorkshopAlert(type, `

${body.trim()}

`), + ); } function rewriteGfmTaskLists(html: string) { @@ -407,6 +369,24 @@ const entryPathStepCounts: Record = Object.fromEntries( ]), ); +function renderEntryPathDescription(path: typeof workshopEntryPaths[number], journeyLabel: string, stepCount: number) { + const stepLabel = `${stepCount} step${stepCount === 1 ? '' : 's'}`; + return escapeHtml(`${journeyLabel} · ${stepLabel} — ${path.kicker}. ${path.summary} ${path.fit}`); +} + +const entryPathTutorialHashes: Record = Object.fromEntries( + workshopEntryPaths.map((path) => { + const flow = buildWorkshopFlow(path.journeyId, workshopDefaults.scenarioId); + const firstStepKey = flow[0] ?? initialStepKey; + const hash = new URLSearchParams({ + j: path.journeyId, + s: workshopDefaults.scenarioId, + ...(firstStepKey ? { t: firstStepKey } : {}), + }).toString(); + return [path.id, `#${hash}`]; + }), +); + ---
= Object.fromEntries( {workshopEntryPaths.map((path) => { const journey = workshopJourneys.find((item) => item.id === path.journeyId) ?? workshopJourneys[0]; return ( - + /> ); })} @@ -808,10 +776,14 @@ const entryPathStepCounts: Record = Object.fromEntries( root.dataset.workshopFlowKeys = visibleFlow.join(','); root.dataset.workshopCurrentStep = activeStep.key; - root.querySelectorAll('[data-workshop-entry-path]').forEach((button) => { - const isActive = button.getAttribute('data-workshop-entry-path') === state.entryPathId; - button.classList.toggle('is-active', isActive); - button.setAttribute('aria-pressed', String(isActive)); + root.querySelectorAll('[data-workshop-entry-path]').forEach((entryPathLink) => { + const isActive = entryPathLink.getAttribute('data-workshop-entry-path') === state.entryPathId; + entryPathLink.classList.toggle('is-active', isActive); + if (isActive) { + entryPathLink.setAttribute('aria-current', 'true'); + } else { + entryPathLink.removeAttribute('aria-current'); + } }); const flowIndex = visibleFlow.indexOf(activeStep.key); @@ -838,6 +810,9 @@ const entryPathStepCounts: Record = Object.fromEntries( } if (target.matches('[data-workshop-entry-path]')) { + // Keep entry-path selection inside the workshop state machine instead + // of letting the browser jump to the raw hash target first. + event.preventDefault(); setJourney(target.getAttribute('data-workshop-journey'), target.getAttribute('data-workshop-entry-path')); showTutorial(); return; @@ -900,9 +875,6 @@ const entryPathStepCounts: Record = Object.fromEntries(