From b4af5b5ec9e14e6469a6130b096e4b5d56ec64f1 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:43:05 +0530 Subject: [PATCH 01/25] feat(engine): define independent stable bilingual URL, SEO and safe-link contract --- scripts/scarp-site/routes.ts | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 scripts/scarp-site/routes.ts diff --git a/scripts/scarp-site/routes.ts b/scripts/scarp-site/routes.ts new file mode 100644 index 0000000..b085a2b --- /dev/null +++ b/scripts/scarp-site/routes.ts @@ -0,0 +1,62 @@ +/** + * Scarp-owned URL contract. This module deliberately imports no site framework. + * Keep this mapping compatible with public Pages URLs when switching engines. + */ +import { posix } from 'node:path' + +export const ORIGIN = 'https://scarp-lang.github.io' +export const BASE = '/scarp-docs/' +export type Locale = 'en' | 'hi' + +export function sourceRoute(source: string): string { + if (!source.endsWith('.md') || source.startsWith('/') || source.includes('\\') || + source.includes('?') || source.includes('#') || + source.split('/').some(s => s === '.' || s === '..' || !s)) { + throw new TypeError('Invalid Scarp documentation path: ' + source) + } + const withoutExtension = source.slice(0, -3) + if (withoutExtension === 'index') return BASE + if (withoutExtension.endsWith('/index')) { + return BASE + withoutExtension.slice(0, -5).split('/').map(encodeURIComponent).join('/') + } + return BASE + withoutExtension.split('/').map(encodeURIComponent).join('/') +} + +export function htmlPath(source: string): string { + if (!source.endsWith('.md') || source.includes('..') || source.startsWith('/') || + source.includes('\\')) throw new TypeError('Invalid output route: ' + source) + return source.slice(0, -3) + '.html' +} + +export function pairedRoutes(source: string): { + locale: Locale; route: string; canonical: string; english: string; hindi: string +} { + const locale: Locale = source.startsWith('hi/') ? 'hi' : 'en' + const name = locale === 'hi' ? source.slice(3) : source + const english = ORIGIN + sourceRoute(name) + const hindi = ORIGIN + sourceRoute('hi/' + name) + return { locale, route: sourceRoute(source), + canonical: ORIGIN + sourceRoute(source), english, hindi } +} + +export function escapeHtml(text: string): string { + return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') + .replace(/'/g, ''') +} + +export function localLink(href: string, pageSource: string): string { + if (!href || href.startsWith('#')) return href + if (/^(?:https?:|mailto:|tel:)/i.test(href)) return href + if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//')) return '#' + if (href.startsWith(BASE)) return href + if (href.startsWith('/')) return BASE + href.slice(1) + const [pathname, fragment = ''] = href.split('#', 2) + const isMarkdown = pathname.endsWith('.md') + if (isMarkdown) { + const normalized = posix.normalize(posix.join(posix.dirname(pageSource), pathname)) + if (normalized.startsWith('../') || normalized.startsWith('/')) return '#' + return sourceRoute(normalized) + (fragment ? '#' + fragment : '') + } + return href +} From 7e5367ee52c277a49c195a05da5edc1f4083a176 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:44:21 +0530 Subject: [PATCH 02/25] feat(engine): implement safe TypeScript Markdown renderer, YAML home data, callouts, code highlighting and interactive placeholders --- scripts/scarp-site/markdown.ts | 174 +++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 scripts/scarp-site/markdown.ts diff --git a/scripts/scarp-site/markdown.ts b/scripts/scarp-site/markdown.ts new file mode 100644 index 0000000..194dc87 --- /dev/null +++ b/scripts/scarp-site/markdown.ts @@ -0,0 +1,174 @@ +import MarkdownIt from 'markdown-it' +import { parse as parseYaml } from 'yaml' +import { documentationHeadingSlug } from '../lib/docs-slug.ts' +import { escapeHtml, localLink } from './routes.ts' + +export interface HomeAction { theme?: string; text: string; link: string } +export interface HomeFeature { title: string; details: string; link: string } +export interface FrontMatter { + layout?: string + title?: string + titleTemplate?: string + hero?: { + name?: string; text?: string; tagline?: string + image?: { src?: string; alt?: string } + actions?: HomeAction[] + } + features?: HomeFeature[] +} + +export interface RenderedPage { + title: string + html: string + bodyText: string + headings: Array<{ id: string; title: string; level: number }> + frontmatter: FrontMatter +} + +export function splitFrontMatter(markdown: string): {matter: FrontMatter; content: string} { + const source = markdown.replace(/^\uFEFF/, '') + if (!source.startsWith('---\n')) return {matter:{}, content:source} + const end = source.indexOf('\n---\n', 4) + if (end === -1) throw new Error('Unclosed YAML front matter') + const raw = parseYaml(source.slice(4, end)) + if (raw !== null && (typeof raw !== 'object' || Array.isArray(raw))) { + throw new TypeError('Documentation front matter must be a YAML object') + } + return {matter:(raw ?? {}) as FrontMatter,content:source.slice(end+5)} +} + +/** + * Translate trusted VitePress-style callout *syntax*, not arbitrary source HTML. + * All normal Markdown is parsed with html:false so a Markdown contribution + * cannot embed script tags, handlers or HTML event attributes. + */ +function containers(source: string): { + markdown: string; trusted: Map +} { + const trusted = new Map() + const stack: string[] = [] + let counter=0 + const lines = source.split('\n') + const output: string[] = [] + for (const line of lines) { + const open = /^::: (tip|info|warning|danger|note|details)(?:\s+(.*))?\s*$/.exec(line) + if (open) { + const kind = open[1] + const name = (open[2] ?? kind).trim() + const token = 'SCARPTRUSTEDBLOCK'+counter+++'TOKEN' + const element = kind === 'details' + ? '
'+escapeHtml(name)+'' + : '
':'') + output.push('',token,'') + continue + } + output.push(line) + } + if (stack.length) throw new Error('Unclosed Markdown callout container') + return {markdown:output.join('\n'),trusted} +} + +function highlightScarp(raw: string): string { + // Lex on the raw text BEFORE escaping: highlighted spans never wrap entities. + const tokens = /(\/\/[^\n]*|"(?:\\.|[^"\\])*"|\b(?:bind|var|funcra|pure|if|elif|else|while|repeat|break|continue|return|needs|ensures|import|as|pub|true|false|integer|float|boolean|string|vector|integers|tile|unit)\b|\b\d+(?:\.\d+)?\b)/g + let out='', previous=0 + for (const match of raw.matchAll(tokens)) { + const index=match.index + out+=escapeHtml(raw.slice(previous,index)) + const value=match[0] + const type=value.startsWith('//')?'comment':value.startsWith('"')?'string': + /^\d/.test(value)?'number':'keyword' + out+=''+escapeHtml(value)+'' + previous=index+value.length + } + return out+escapeHtml(raw.slice(previous)) +} + +function widgetPlaceholders(source: string): {markdown:string;trusted:Map} { + const trusted=new Map() + const widgets: Array<[RegExp,string,string]> = [ + [//g,'SCARPWIDGETCOMMANDS','
'], + [//g,'SCARPWIDGETDEPLOYMENTS', + '

GitHub Pages deployment history is available from GitHub Actions.

'] + ] + let markdown=source + for (const [pattern,token,html] of widgets) { + if (pattern.test(markdown)) { + trusted.set(token,html) + markdown=markdown.replace(pattern,'\n'+token+'\n') + } + } + return {markdown,trusted} +} + +export function renderMarkdown(markdown: string, source: string): RenderedPage { + const {matter,content} = splitFrontMatter(markdown) + const headings: RenderedPage['headings'] = [] + const anchorCounts=new Map() + const renderer=new MarkdownIt({html:false,linkify:true,typographer:false,breaks:false}) + const previousLink=renderer.renderer.rules.link_open + renderer.renderer.rules.link_open=(tokens,i,opts,env,self)=>{ + const token=tokens[i] + const raw=token.attrGet('href') + if (raw) { + const next=localLink(raw,source) + token.attrSet('href',next) + if (/^https?:\/\//.test(next)) { + token.attrSet('rel','noopener noreferrer') + } + } + return previousLink + ? previousLink(tokens,i,opts,env,self) + : self.renderToken(tokens,i,opts) + } + renderer.renderer.rules.image=(tokens,i,opts,env,self)=>{ + const token=tokens[i] + const src=token.attrGet('src') + if (src) token.attrSet('src',localLink(src,source)) + token.attrSet('loading','lazy') + return self.renderToken(tokens,i,opts) + } + renderer.renderer.rules.heading_open=(tokens,i,opts,env,self)=>{ + const token=tokens[i] + const inline=tokens[i+1] + const title=(inline?.children??[]).map(child=>child.type==='image' + ?child.content:(child.type==='text'||child.type==='code_inline'?child.content:'')).join('').trim() + const base=documentationHeadingSlug(title)||'section' + const number=anchorCounts.get(base)??0 + anchorCounts.set(base,number+1) + const id=number?base+'-'+number:base + token.attrSet('id',id) + headings.push({id,title,level:Number(token.tag.slice(1))}) + return self.renderToken(tokens,i,opts) + } + renderer.renderer.rules.fence=(tokens,i)=>{ + const token=tokens[i] + const lang=token.info.trim().split(/\s+/)[0].replace(/[^a-zA-Z0-9_-]/g,'').toLowerCase() + const rendered=lang==='scarp'||lang==='srp' + ?highlightScarp(token.content):escapeHtml(token.content) + return '
'+rendered+'
\n' + } + const widgets=widgetPlaceholders(content) + const blocks=containers(widgets.markdown) + let html=renderer.render(blocks.markdown) + const placeholders=new Map([...widgets.trusted,...blocks.trusted]) + for (const [token, trusted] of placeholders) { + html=html.replaceAll('

'+token+'

',trusted) + } + // Trust no raw author HTML: only the explicitly handled widget/callout tags + // are ever injected AFTER markdown-it's HTML-escaping parse. + const firstHeading=headings.find(h=>h.level===1) + const title=matter.title??firstHeading?.title??'Scarp' + const bodyText=content.replace(/<[^>]+>/g,' ') + .replace(/[#*_\x60~|]/g,' ').replace(/\s+/g,' ').trim() + return {title,html,bodyText,headings,frontmatter:matter} +} From 2fdec7082c7aa9f2f013632066117b176dfe3ab7 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:46:24 +0530 Subject: [PATCH 03/25] feat(engine): build all 138 bilingual pages with Scarp-owned static layout, SEO, sitemap, search index, styles and JS --- scripts/scarp-site/build.ts | 197 ++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 scripts/scarp-site/build.ts diff --git a/scripts/scarp-site/build.ts b/scripts/scarp-site/build.ts new file mode 100644 index 0000000..4e8122c --- /dev/null +++ b/scripts/scarp-site/build.ts @@ -0,0 +1,197 @@ +import assert from 'node:assert/strict' +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { build as bundle } from 'esbuild' +import { documentationPages } from '../lib/docs-pages.ts' +import { commandAdvice, FINDER_TEXT, RELEASES, TASKS } from '../../docs/.vitepress/theme/lib/command-finder.ts' +import { escapeHtml, localLink, htmlPath, pairedRoutes, BASE } from './routes.ts' +import { renderMarkdown, type RenderedPage, type HomeAction, type HomeFeature } from './markdown.ts' + +const root=resolve(fileURLToPath(new URL('../..',import.meta.url))) +const docsRoot=join(root,'docs') +export const previewRoot=join(root,'artifacts','scarp-engine-preview') +const siteRoot=join(previewRoot,'scarp-docs') +const styles=join(root,'scripts/scarp-site/site.css') +const client=join(root,'scripts/scarp-site/client.ts') +const groupOrder=['guide','tutorials','language','tooling','reference','examples','releases','community','project'] +const groupLabels:Record={ + guide:['Getting started','शुरुआत'], + tutorials:['Verified tutorials','सत्यापित ट्यूटोरियल'], + language:['Language','भाषा'], + tooling:['Developer tools','डेवलपर टूल्स'], + reference:['Reference','संदर्भ'], + examples:['Examples','उदाहरण'], + releases:['Published releases','प्रकाशित रिलीज़'], + community:['Community','समुदाय'], + project:['Maintainers','अनुरक्षक'] +} +const logo=''+ + ''+ + '' + +interface IndexEntry {route:string;title:string;locale:'en'|'hi';section:string;excerpt:string} +interface PageView {source:string;page:RenderedPage;route:string;locale:'en'|'hi'} + +function htmlAttr(text:string):string{return escapeHtml(text)} +function attrs(href:string, cls?:string):string { + return 'href="'+htmlAttr(href)+'"'+(cls?' class="'+htmlAttr(cls)+'"':'') +} +function localized(en:string,hi:string,locale:'en'|'hi'):string{return locale==='hi'?hi:en} +function groupFor(source:string):string { + const path=source.startsWith('hi/')?source.slice(3):source + return path.split('/')[0]==='index.md'?'guide':path.split('/')[0] +} +function homeActions(items: HomeAction[]|undefined, source:string):string { + return (items??[]).map(action=>''+ + escapeHtml(action.text)+'').join('') +} +function homeFeatures(items:HomeFeature[]|undefined,source:string):string{ + return '
'+(items??[]).map(item=> + '

'+escapeHtml(item.title)+'

'+ + '

'+escapeHtml(item.details)+'

').join('')+'
' +} + +function homeMarkup(page:RenderedPage,source:string):string { + const hero=page.frontmatter.hero + if (!hero || page.frontmatter.layout!=='home') return page.html + return '
Scarp'+ + '

'+escapeHtml(hero.name??'Scarp')+'

'+ + '

'+escapeHtml(hero.text??page.title)+'

'+ + '

'+escapeHtml(hero.tagline??'')+'

'+ + '
'+homeActions(hero.actions,source)+'
'+ + homeFeatures(page.frontmatter.features,source)+page.html +} +function navigation(locale:'en'|'hi',active:string,views:PageView[]):string { + const inLocale=views.filter(v=>v.locale===locale && v.source!=='index.md' && v.source!=='hi/index.md') + const byGroup=new Map() + for(const v of inLocale){ + const key=groupFor(v.source) + if(!byGroup.has(key))byGroup.set(key,[]) + byGroup.get(key)!.push(v) + } + return '' +} +function finderData():string { + const entries=RELEASES.flatMap(release=> + TASKS.map(task=>commandAdvice(release.tag,task.id))) + const json=JSON.stringify({releases:RELEASES,tasks:TASKS,entries,text:FINDER_TEXT}) + .replace(/'+json+'' +} + +function htmlDocument(view:PageView, views:PageView[]):string{ + const {page,source,route,locale}=view + const meta=pairedRoutes(source) + const other=locale==='en'?meta.hindi:meta.english + const home=page.frontmatter.layout==='home' + const body=home?homeMarkup(page,source):page.html + const heading=home?'':('

'+escapeHtml(page.title)+'

') + const seoDescription=(home?page.frontmatter.hero?.tagline:page.bodyText)??page.bodyText + const description=seoDescription.slice(0,185) + const lang=locale==='hi'?'hi-IN':'en-US' + const target=other.slice('https://scarp-lang.github.io'.length) + const localeText=localized('हिन्दी','English',locale) + const title=page.title==='Scarp'?'Scarp Docs':page.title+' · Scarp Docs' + const searchText=localized('Search documentation','दस्तावेज़ खोजें',locale) + const contents=page.headings.filter(h=>h.level===2||h.level===3).slice(0,45) + const toc=contents.length + ?'':'' + const finder=body.includes('data-command-finder')?finderData():'' + return ''+ + ''+ + ''+ + ''+escapeHtml(title)+''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '
'+navigation(locale,route,views)+ + '
'+heading+body+'
'+toc+'
'+ + ''+ + '

'+searchText+'

'+ + '
'+ + ''+ + ''+ + '

    '+ + finder+ + '
    '+localized('Experimental Scarp language. Verify each guide against its published compiler release.', + 'Scarp भाषा प्रयोगात्मक है। हर गाइड का प्रकाशित compiler version जाँचें।',locale)+ + ' · MIT documentation source
    '+ + '' +} + +export async function buildSite():Promise<{pages:number;assets:number}>{ + // The engine preview NEVER overwrites VitePress production output. + rmSync(previewRoot,{recursive:true,force:true}) + mkdirSync(join(siteRoot,'assets'),{recursive:true}) + const sources=documentationPages(docsRoot) + const views=sources.map(source=>{ + const page=renderMarkdown(readFileSync(join(docsRoot,source),'utf8'),source) + return {page,source,route:pairedRoutes(source).route,locale:pairedRoutes(source).locale} + }) + assert.equal(views.length,138,'Migration must preserve all 138 paired source pages') + assert.equal(views.filter(v=>v.locale==='hi').length,69,'Hindi page parity') + const routes=new Set(views.map(v=>v.route)) + assert.equal(routes.size,views.length,'Generated public routes must be unique') + const searchIndex:IndexEntry[]=views.map(view=>({ + route:view.route,title:view.page.title,locale:view.locale, + section:groupFor(view.source),excerpt:view.page.bodyText.slice(0,2500) + })) + for(const view of views){ + const dest=join(siteRoot,htmlPath(view.source)) + mkdirSync(resolve(dest,'..'),{recursive:true}) + writeFileSync(dest,htmlDocument(view,views),'utf8') + } + const sitemap='\n'+ + '\n'+ + views.map(v=>''+pairedRoutes(v.source).canonical+'').join('\n')+ + '\n\n' + writeFileSync(join(siteRoot,'sitemap.xml'),sitemap) + writeFileSync(join(siteRoot,'assets','search-index.json'),JSON.stringify(searchIndex)) + writeFileSync(join(siteRoot,'assets','scarp-mark.svg'),logo) + writeFileSync(join(siteRoot,'assets','scarp-site.css'),readFileSync(styles,'utf8')) + await bundle({ + entryPoints:[client],outfile:join(siteRoot,'assets','scarp-site.js'), + bundle:true,platform:'browser',format:'iife',target:'es2022',minify:true,logLevel:'silent' + }) + writeFileSync(join(siteRoot,'404.html'),'Not found · Scarp Docs'+ + '

    Documentation page not found

    Scarp Docs
    ') + console.log('PASS: independent Scarp TypeScript engine generated '+views.length+ + ' English/Hindi Markdown routes, local CSS/JavaScript and sitemap under '+siteRoot) + return {pages:views.length,assets:4} +} +if(process.argv[1] && resolve(process.argv[1])===fileURLToPath(import.meta.url)){ + await buildSite() +} From ace2e0544b6c7e92094ea483b538ff789b0e83bc Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:47:57 +0530 Subject: [PATCH 04/25] feat(engine): create independent responsive Scarp Docs design system with accessible dark mode and bilingual navigation --- scripts/scarp-site/site.css | 139 ++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 scripts/scarp-site/site.css diff --git a/scripts/scarp-site/site.css b/scripts/scarp-site/site.css new file mode 100644 index 0000000..38f6174 --- /dev/null +++ b/scripts/scarp-site/site.css @@ -0,0 +1,139 @@ +/* Scarp Docs' own responsive design system. No VitePress/theme CSS. */ +:root { + color-scheme: light; + --background: #ffffff; --surface: #f8fafc; --surface-strong: #eff6ff; + --text: #17223a; --muted: #475569; --line: #dbe3ef; + --accent: #1d4ed8; --accent-hover: #1e40af; --code-bg: #eff3f9; + --shadow: 0 12px 40px rgba(15, 23, 42, 0.11); +} +html.dark { + color-scheme: dark; --background: #101722; --surface: #172233; + --surface-strong: #1a2a45; --text: #f3f7ff; --muted: #c3d1e3; + --line: #35445c; --accent: #93c5fd; --accent-hover: #bfdbfe; + --code-bg: #1b2a3e; --shadow: 0 14px 48px rgba(0,0,0,0.37); +} +* { box-sizing: border-box; } +html { scroll-behavior: smooth; scroll-padding-top: 6.5rem; } +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { transition-duration: 0s !important; animation-duration: 0s !important; } } +body { margin:0; font: 16px/1.7 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; color:var(--text); background:var(--background); overflow-wrap:anywhere; } +a { color:var(--accent); text-decoration-thickness:.06em; text-underline-offset:.15em; } +a:hover { text-decoration: underline; } +button,input,select { font:inherit; } +button,select { cursor:pointer; } +button,select,input { color:var(--text); background:var(--background); border:1px solid var(--line); border-radius:8px; } +button { padding:.4rem .7rem; } +button:hover,select:hover { border-color:var(--accent); } +:focus-visible { outline:3px solid var(--accent); outline-offset:3px; } +.skip-link { position:fixed; top:-100px; left:12px; z-index:1000; padding:.5rem 1rem; background:var(--surface); border:2px solid var(--accent); } +.skip-link:focus { top:12px; } +.site-header { height:66px; border-bottom:1px solid var(--line); display:flex; gap:clamp(.5rem,2vw,2rem); align-items:center; justify-content:space-between; position:sticky; top:0; padding:.3rem clamp(.7rem,2.5vw,2.4rem); background:var(--background); z-index:20; } +.brand { display:flex; gap:.55rem; align-items:center; font-size:1.22rem; font-weight:800; letter-spacing:.02em; text-decoration:none; color:var(--text); } +.brand img { border-radius:6px; } +.top-nav { display:flex; gap:clamp(.5rem,1.7vw,1.5rem); margin-right:auto; } +.top-nav a { text-decoration:none; color:var(--muted); font-weight:600; font-size:.94rem; } +.top-nav a:hover { color:var(--accent); } +.header-actions { display:flex; gap:.5rem; align-items:center; white-space:nowrap; } +.header-actions button { font-size:.91rem; min-height:38px; } +.header-actions .language-link { display:inline-flex; align-items:center; padding:0 .5rem; min-height:38px; font-weight:600; } +#menu-toggle { display:none; } +.site-layout { max-width:1600px; margin:auto; padding:0 1rem; display:grid; grid-template-columns:minmax(220px,255px) minmax(0,1fr) minmax(170px,205px); gap:clamp(1rem,2.2vw,2.8rem); align-items:start; } +.side-nav { border-right:1px solid var(--line); padding:1.3rem .75rem 2rem 0; position:sticky; top:66px; max-height:calc(100vh - 66px); overflow-y:auto; scrollbar-width:thin; } +.side-nav a { text-decoration:none; display:block; padding:.22rem .6rem; color:var(--muted); border-radius:6px; line-height:1.45; font-size:.87rem; } +.side-nav a:hover,.side-nav a.active { background:var(--surface-strong); color:var(--accent); } +.side-nav a.active { font-weight:750; } +.side-home { font-weight:750; } +.nav-group { margin-top:1rem; } +.nav-group h2 { font-size:.73rem; color:var(--text); text-transform:uppercase; letter-spacing:.08em; margin:.75rem .55rem .35rem; } +.nav-group ul,.toc ul { list-style:none; padding:0; margin:0; } +.nav-group li { margin:.14rem 0; } +main { min-width:0; } +.doc-content { max-width:860px; min-height:70vh; margin:2.4rem auto 3rem; padding:0 .25rem; } +.doc-content > :first-child { margin-top:0; } +.doc-content h1 { font-size:clamp(1.7rem,3vw,2.7rem); line-height:1.24; margin:1.6rem 0 1rem; letter-spacing:-.025em; } +.doc-content h2 { font-size:clamp(1.24rem,2vw,1.65rem); margin:2.4rem 0 .75rem; line-height:1.3; padding-bottom:.45rem; border-bottom:1px solid var(--line); } +.doc-content h3 { font-size:1.16rem; margin:2rem 0 .6rem; } +.doc-content h4 { font-size:1rem; } +.doc-content p { margin:.85rem 0 1rem; } +.doc-content ul,.doc-content ol { padding-left:1.55rem; } +.doc-content li { margin:.3rem 0; } +.doc-content blockquote { margin:1.5rem 0; padding:.1rem 1.15rem; border-left:4px solid var(--accent); background:var(--surface); } +.doc-content table { width:max-content; max-width:100%; border-spacing:0; border-collapse:collapse; display:block; overflow-x:auto; font-size:.9rem; } +.doc-content th,.doc-content td { border:1px solid var(--line); padding:.55rem .75rem; text-align:start; min-width:65px; } +.doc-content th { background:var(--surface); } +.doc-content img { max-width:100%; height:auto; } +.doc-content code:not(pre code) { border-radius:.3rem; padding:.13em .32em; background:var(--code-bg); font: .91em/1.5 ui-monospace,Consolas,monospace; } +.doc-content pre,.command-finder pre { max-width:100%; overflow-x:auto; padding:1rem; background:var(--code-bg); border:1px solid var(--line); border-radius:.6rem; margin:1rem 0; tab-size:2; font: .89rem/1.5 ui-monospace,Consolas,"Liberation Mono",monospace; white-space:pre; overflow-wrap:normal; } +.doc-content pre code { white-space:inherit; } +.syntax-keyword { color:#1d4ed8; font-weight:600; } +.syntax-number { color:#9a3412; } +.syntax-string { color:#047857; } +.syntax-comment { color:#64748b; font-style:italic; } +.dark .syntax-keyword { color:#a5b4fc; } +.dark .syntax-number { color:#fdba74; } +.dark .syntax-string { color:#6ee7b7; } +.dark .syntax-comment { color:#cbd5e1; } +.scarp-callout { display:block; padding:.8rem 1rem; margin:1.35rem 0; border-radius:.55rem; border-left:4px solid var(--accent); background:var(--surface-strong); } +.scarp-warning,.scarp-danger { border-left-color:#b45309; } +.scarp-callout > strong { display:block; margin-bottom:.5rem; } +.toc { position:sticky; top:82px; max-height:calc(100vh - 90px); overflow-y:auto; padding:1.3rem 0; font-size:.82rem; } +.toc h2 { font-size:.82rem; color:var(--muted); } +.toc a { text-decoration:none; display:block; border-left:2px solid var(--line); padding:.2rem .6rem; color:var(--muted); } +.toc a:hover { color:var(--accent); border-left-color:var(--accent); } +.toc-level-3 { margin-left:.6rem; } +.site-footer { border-top:1px solid var(--line); padding:1.2rem 2rem; text-align:center; color:var(--muted); font-size:.84rem; } +.hero { text-align:center; padding:2.8rem 0 1rem; } +.hero img { border-radius:20px; } +.hero-kicker { color:var(--accent); font-weight:750; letter-spacing:.1em; text-transform:uppercase; } +.hero h1 { font-size:clamp(2.2rem,5vw,4rem); margin:.5rem 0; } +.hero > p:last-of-type { max-width:700px; margin:1rem auto; color:var(--muted); font-size:1.08rem; } +.hero-actions { display:flex; justify-content:center; flex-wrap:wrap; gap:.8rem; margin:1.5rem auto 2rem; } +.hero-action { display:inline-flex; align-items:center; justify-content:center; text-decoration:none; font-weight:650; padding:.6rem 1.2rem; border-radius:.55rem; border:1px solid var(--line); color:var(--text); } +.hero-action.primary { background:#1d4ed8; border-color:#1d4ed8; color:#fff; } +.hero-action.primary:hover { background:#1e40af; } +.features { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,230px),1fr)); gap:.85rem; margin:1.6rem 0 2.4rem; } +.feature { display:block; color:var(--text); border:1px solid var(--line); border-radius:.85rem; padding:1rem 1.15rem; text-decoration:none; background:var(--surface); } +.feature:hover { border-color:var(--accent); text-decoration:none; } +.feature h2 { margin:.1rem 0 .55rem; padding:0; font-size:1.1rem; border:0; color:var(--accent); } +.feature p { margin:0; color:var(--muted); font-size:.93rem; } +.search-panel { width:min(700px,calc(100vw - 1.5rem)); max-height:80vh; margin:10vh auto; border:1px solid var(--line); border-radius:.9rem; padding:1rem 1.25rem; background:var(--background); color:var(--text); box-shadow:var(--shadow); } +.search-panel::backdrop { background:rgba(15,23,42,.62); } +.search-panel:not([open]) { display:none; } +.search-header { display:flex; align-items:center; justify-content:space-between; } +.search-header h2 { margin:0 0 .6rem; font-size:1.3rem; } +.search-panel label { display:block; font-size:.85rem; margin-bottom:.35rem; color:var(--muted); } +#search-query { display:block; width:100%; padding:.65rem .85rem; } +#search-status { color:var(--muted); font-size:.85rem; } +#search-results { list-style:none; padding:0; margin:.25rem 0; max-height:46vh; overflow-y:auto; } +#search-results li { border-bottom:1px solid var(--line); } +#search-results a { display:block; padding:.55rem .45rem; border-radius:.4rem; text-decoration:none; } +#search-results a:hover,#search-results a:focus { background:var(--surface); } +#search-results small { display:block; color:var(--muted); font-size:.8rem; } +.command-finder { border:1px solid var(--line); padding:1rem; border-radius:.85rem; background:var(--surface); margin:1.5rem 0; } +.command-finder h2 { margin:0 0 .5rem; border:0; } +.command-finder .fields { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:1rem; } +.command-finder label { display:block; margin-bottom:.35rem; font-weight:600; } +.command-finder select { width:100%; padding:.6rem; } +.command-finder .status { font-weight:700; } +.command-finder .status.unavailable { color:#b45309; } +.command-finder button { background:#1d4ed8; color:#fff; } +.deployment button { margin:.5rem 0; } +@media(max-width:1270px) { .site-layout { grid-template-columns:235px minmax(0,1fr); } .toc { display:none; } } +@media(max-width:900px) { + .site-header { height:62px; gap:.5rem; } + .top-nav { display:none; } + .site-layout { display:block; padding:0 1rem; } + .side-nav { display:none; position:fixed; z-index:30; top:62px; left:0; bottom:0; max-height:calc(100vh - 62px); width:min(335px,90vw); padding:1rem; border:1px solid var(--line); background:var(--background); box-shadow:var(--shadow); } + .side-nav.open { display:block; } + #menu-toggle { display:inline-block; } + .doc-content { margin:1.7rem auto; } + #search-open kbd { display:none; } +} +@media(max-width:480px) { + .site-header { padding:.25rem .55rem; } + .brand span { font-size:1rem; } + #search-open { font-size:0; padding:.4rem; min-width:42px; } + #search-open::before { content:"⌕"; font-size:1.35rem; } + .header-actions { gap:.15rem; } + .header-actions .language-link { padding:.25rem; } + .site-layout { padding:0 .8rem; } +} From 5c5881f60dd933ab3e21e8186c293d26054cb979 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:49:25 +0530 Subject: [PATCH 05/25] feat(engine): implement framework-free TypeScript browser search, theme, mobile navigation and release-aware CLI widget --- scripts/scarp-site/client.ts | 286 +++++++++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 scripts/scarp-site/client.ts diff --git a/scripts/scarp-site/client.ts b/scripts/scarp-site/client.ts new file mode 100644 index 0000000..84e528b --- /dev/null +++ b/scripts/scarp-site/client.ts @@ -0,0 +1,286 @@ +/** + * Scarp's own browser controller. No Vue/VitePress runtime. + * The server renders every page as plain HTML; this enhances only the search, + * appearance, mobile menu, deployment count and release-aware CLI widget. + */ +interface SearchEntry { route: string; title: string; locale: 'en'|'hi'; section: string; excerpt: string } +interface Release { tag:string; label:string } +interface Task { id:string; en:string; hi:string } +interface CommandAdvice { + release:string;task:string;supported:boolean;firstRelease:string; + commands:string[];reference:string;note:string +} +interface LocalStrings { + heading:string; intro:string; version:string; task:string; supported:string; + unavailable:string; first:string; command:string; copy:string; copied:string; + copyFailed:string; reference:string; missing:string; warning:string; + notes:Record +} +interface FinderData { + releases:Release[];tasks:Task[];entries:CommandAdvice[]; + text:{en:LocalStrings;hi:LocalStrings} +} +const BASE='/scarp-docs/' +const hindi=document.documentElement.lang.startsWith('hi') +const locale:'en'|'hi'=hindi?'hi':'en' +const t=(en:string,hi:string):string=>hindi?hi:en +function el( + tag:K,cls?:string,text?:string +):HTMLElementTagNameMap[K]{ + const node=document.createElement(tag) + if(cls)node.className=cls + if(text!==undefined)node.textContent=text + return node +} + +/** Restore persisted appearance without relying on third-party theme scripts. */ +function installAppearance():void { + const toggle=document.querySelector('#theme-toggle') + if(!toggle)return + let stored:string|null=null + try{stored=localStorage.getItem('scarp-docs-appearance')}catch{ /* disabled storage */ } + const initial=stored==='dark'||(stored!== 'light'&& + window.matchMedia('(prefers-color-scheme: dark)').matches) + const apply=(dark:boolean):void=>{ + document.documentElement.classList.toggle('dark',dark) + toggle.setAttribute('aria-pressed',String(dark)) + toggle.setAttribute('aria-label',dark?t('Use light theme','लाइट मोड चुनें'): + t('Use dark theme','डार्क मोड चुनें')) + } + apply(initial) + toggle.addEventListener('click',()=>{ + const next=!document.documentElement.classList.contains('dark') + apply(next) + try{localStorage.setItem('scarp-docs-appearance',next?'dark':'light')} + catch{ /* private browsing */ } + }) +} + +function installMenu():void { + const toggle=document.querySelector('#menu-toggle') + const navigation=document.querySelector('#side-nav') + if(!toggle||!navigation)return + const setOpen=(open:boolean):void=>{ + navigation.classList.toggle('open',open) + toggle.setAttribute('aria-expanded',String(open)) + } + toggle.addEventListener('click',()=>setOpen(!navigation.classList.contains('open'))) + navigation.addEventListener('click',event=>{ + if((event.target as Element).closest('a'))setOpen(false) + }) + document.addEventListener('keydown',event=>{ + if(event.key==='Escape'&&navigation.classList.contains('open')){ + setOpen(false) + toggle.focus() + } + }) +} + +let searchIndexPromise:Promise|null=null +function searchIndex():Promise { + if(!searchIndexPromise){ + searchIndexPromise=fetch(BASE+'assets/search-index.json').then(response=>{ + if(!response.ok)throw new Error('Local search index unavailable') + return response.json() as Promise + }).catch(err=>{searchIndexPromise=null;throw err}) + } + return searchIndexPromise +} +function search(entries:SearchEntry[],value:string):SearchEntry[]{ + const terms=value.toLocaleLowerCase().split(/\s+/u).filter(Boolean) + if(terms.length===0)return [] + const scored=entries.filter(e=>e.locale===locale).map(e=>{ + const heading=e.title.toLocaleLowerCase() + const content=e.excerpt.toLocaleLowerCase() + if(!terms.every(term=>heading.includes(term)||content.includes(term)))return null + const score=terms.reduce((sum,term)=>sum+ + (heading===term?100:heading.startsWith(term)?50:heading.includes(term)?22:0)+ + (content.includes(term)?1:0),0) + return {entry:e,score} + }).filter((x):x is {entry:SearchEntry;score:number}=>x!==null) + return scored.sort((a,b)=>b.score-a.score||a.entry.title.localeCompare(b.entry.title)) + .slice(0,18).map(x=>x.entry) +} + +function installSearch():void { + const dialog=document.querySelector('#search-panel') + const open=document.querySelector('#search-open') + const close=document.querySelector('#search-close') + const input=document.querySelector('#search-query') + const output=document.querySelector('#search-results') + const status=document.querySelector('#search-status') + if(!dialog||!open||!close||!input||!output||!status)return + let previousFocus:HTMLElement|null=null + let generation=0 + + const refresh=async():Promise=>{ + const current=++generation + const query=input.value.trim() + output.replaceChildren() + if(!query){ + status.textContent=t('Start typing to search the documentation.','खोजने के लिए शब्द लिखें।') + return + } + status.textContent=t('Searching local documentation…','स्थानीय दस्तावेज़ खोजे जा रहे हैं…') + try { + const entries=search(await searchIndex(),query) + if(current!==generation||!dialog.open)return + status.textContent=entries.length + ?t(entries.length+' result(s)',entries.length+' परिणाम') + :t('No results found','कोई परिणाम नहीं मिला') + for(const entry of entries){ + const li=el('li') + const link=el('a') + link.href=entry.route + link.appendChild(el('strong',undefined,entry.title)) + link.appendChild(el('small',undefined,entry.excerpt.slice(0,140))) + li.appendChild(link) + output.appendChild(li) + } + }catch{ + if(current===generation)status.textContent= + t('Could not load the local search index. Use page navigation instead.', + 'स्थानीय खोज उपलब्ध नहीं है। पृष्ठों के लिंक का उपयोग करें।') + } + } + const show=():void=>{ + if(dialog.open)return + previousFocus=document.activeElement instanceof HTMLElement?document.activeElement:open + dialog.showModal() + input.focus() + void refresh() + } + open.addEventListener('click',show) + close.addEventListener('click',()=>dialog.close()) + dialog.addEventListener('click',event=>{ + if(event.target===dialog)dialog.close() + }) + dialog.addEventListener('close',()=>{ + generation++ + if(previousFocus?.isConnected)previousFocus.focus({preventScroll:true}) + else open.focus({preventScroll:true}) + }) + input.addEventListener('input',()=>{void refresh()}) + document.addEventListener('keydown',event=>{ + const target=event.target as HTMLElement + const typing=target.closest('input,textarea,select,[contenteditable="true"]') + if((event.ctrlKey||event.metaKey)&&event.key.toLowerCase()==='k'){ + event.preventDefault() + show() + }else if(event.key==='/'&&!typing&&!dialog.open&&!event.ctrlKey&&!event.metaKey){ + event.preventDefault() + show() + } + }) +} + +function installFinder():void { + const panel=document.querySelector('[data-command-finder]') + const data=document.querySelector('#scarp-command-data') + if(!panel||!data?.textContent)return + let parsed:FinderData + try{parsed=JSON.parse(data.textContent) as FinderData} + catch{panel.textContent=t('Command data unavailable.','Command data उपलब्ध नहीं।');return} + const words=parsed.text[locale] + const h=el('h2',undefined,words.heading) + const intro=el('p',undefined,words.intro) + const fields=el('div','fields') + const versionField=el('div') + const releaseLabel=el('label',undefined,words.version) + releaseLabel.htmlFor='scarp-release' + const releaseSelect=el('select') as HTMLSelectElement + releaseSelect.id='scarp-release' + for(const release of parsed.releases){ + const option=el('option',undefined,release.label) + option.value=release.tag + releaseSelect.appendChild(option) + } + releaseSelect.value=parsed.releases.at(-1)?.tag??'' + const taskField=el('div') + const taskLabel=el('label',undefined,words.task) + taskLabel.htmlFor='scarp-task' + const taskSelect=el('select') as HTMLSelectElement + taskSelect.id='scarp-task' + for(const task of parsed.tasks){ + const option=el('option',undefined,task[locale]) + option.value=task.id + taskSelect.appendChild(option) + } + versionField.append(releaseLabel,releaseSelect) + taskField.append(taskLabel,taskSelect) + fields.append(versionField,taskField) + const result=el('div','finder-result') + result.setAttribute('aria-live','polite') + result.setAttribute('aria-atomic','true') + panel.append(h,intro,fields,result) + + const render=():void=>{ + const item=parsed.entries.find(entry=>entry.release===releaseSelect.value&& + entry.task===taskSelect.value) + if(!item)return + result.replaceChildren() + const status=el('p','status'+(item.supported?'':' unavailable'), + item.supported?words.supported:words.unavailable) + result.appendChild(status) + if(!item.supported){ + result.append(el('p',undefined,words.first+' '+item.firstRelease), + el('p',undefined,words.missing)) + }else{ + result.appendChild(el('p',undefined,words.command)) + const pre=el('pre') + pre.tabIndex=0 + pre.appendChild(el('code',undefined,item.commands.join('\n'))) + result.appendChild(pre) + const button=el('button',undefined,words.copy) + button.type='button' + button.addEventListener('click',()=>{ + if(!navigator.clipboard?.writeText){ + button.textContent=words.copyFailed + return + } + void navigator.clipboard.writeText(item.commands.join('\n')) + .then(()=>{button.textContent=words.copied}) + .catch(()=>{button.textContent=words.copyFailed}) + }) + result.appendChild(button) + result.appendChild(el('p',undefined,words.notes[item.note]??'')) + } + const reference=el('a',undefined,words.reference) + reference.href=BASE+item.reference.replace(/^\/+/,'') + result.appendChild(reference) + result.appendChild(el('p',undefined,words.warning)) + } + releaseSelect.addEventListener('change',render) + taskSelect.addEventListener('change',render) + render() +} + +function installDeployments():void { + const target=document.querySelector('[data-deployments]') + const button=target?.querySelector('[data-load-deployments]') + const output=target?.querySelector('output') + if(!button||!output)return + button.addEventListener('click',()=>{ + button.disabled=true + output.textContent=t('Checking published workflow history…','प्रकाशित workflow इतिहास देख रहे हैं…') + void fetch('https://api.github.com/repos/scarp-lang/scarp-docs/actions/workflows/pages.yml/runs?status=success&per_page=1') + .then(async response=>{ + if(!response.ok)throw new Error('Public Actions API rate limit or network unavailable') + return response.json() as Promise<{total_count?:number}> + }).then(body=>{ + output.textContent=typeof body.total_count==='number' + ?t('Successful Pages workflow runs: '+body.total_count, + 'सफल Pages workflow रन: '+body.total_count) + :t('Run count unavailable','रन की संख्या उपलब्ध नहीं') + }).catch(()=>{ + output.textContent=t('Count unavailable offline. View GitHub Actions when connected.', + 'ऑफलाइन संख्या उपलब्ध नहीं। इंटरनेट पर GitHub Actions देखें।') + }).finally(()=>{button.disabled=false}) + }) +} + +installAppearance() +installMenu() +installSearch() +installFinder() +installDeployments() From 6daaea67fb799b5483b85acceeaf96cd85135951 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:49:50 +0530 Subject: [PATCH 06/25] build(engine): compile standalone browser TypeScript directly without any website framework or bundler --- scripts/scarp-site/build.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/scarp-site/build.ts b/scripts/scarp-site/build.ts index 4e8122c..0be65c1 100644 --- a/scripts/scarp-site/build.ts +++ b/scripts/scarp-site/build.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict' import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import { build as bundle } from 'esbuild' +import ts from 'typescript' import { documentationPages } from '../lib/docs-pages.ts' import { commandAdvice, FINDER_TEXT, RELEASES, TASKS } from '../../docs/.vitepress/theme/lib/command-finder.ts' import { escapeHtml, localLink, htmlPath, pairedRoutes, BASE } from './routes.ts' @@ -182,10 +182,15 @@ export async function buildSite():Promise<{pages:number;assets:number}>{ writeFileSync(join(siteRoot,'assets','search-index.json'),JSON.stringify(searchIndex)) writeFileSync(join(siteRoot,'assets','scarp-mark.svg'),logo) writeFileSync(join(siteRoot,'assets','scarp-site.css'),readFileSync(styles,'utf8')) - await bundle({ - entryPoints:[client],outfile:join(siteRoot,'assets','scarp-site.js'), - bundle:true,platform:'browser',format:'iife',target:'es2022',minify:true,logLevel:'silent' + // Browser controller is a single import-free TypeScript file. Compile it + // directly: no VitePress, Vite, Vue, esbuild or client framework. + const browserScript=ts.transpileModule(readFileSync(client,'utf8'),{ + fileName:'client.ts', + compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.None}, + reportDiagnostics:true }) + if(browserScript.diagnostics?.length)throw new Error('Client TypeScript compilation failed') + writeFileSync(join(siteRoot,'assets','scarp-site.js'),browserScript.outputText) writeFileSync(join(siteRoot,'404.html'),'Not found · Scarp Docs'+ '

    Documentation page not found

    Scarp Docs
    ') console.log('PASS: independent Scarp TypeScript engine generated '+views.length+ From fe41593279d2de408946716d32e855e7e3f73bc5 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:50:23 +0530 Subject: [PATCH 07/25] build(engine): add independent Scarp static-site commands and Markdown/YAML parsing libraries --- package.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index dfe11f9..8aad55c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,9 @@ "docs:dev": "vitepress dev docs --host 127.0.0.1", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", + "scarp:build": "tsx scripts/scarp-site/build.ts", + "scarp:test": "tsx scripts/scarp-site/test.ts", + "scarp:browser-test": "tsx scripts/scarp-site/browser-test.ts", "package:offline": "tsx scripts/package-offline.ts", "test:offline": "tsx scripts/test-offline.ts", "test:browser-offline": "tsx scripts/test-browser-offline.ts", @@ -101,7 +104,10 @@ "tsx": "4.23.15", "vue-tsc": "3.3.13", "playwright": "1.64.1", - "axe-core": "4.14.0" + "axe-core": "4.14.0", + "markdown-it": "14.1.0", + "@types/markdown-it": "14.1.2", + "yaml": "2.8.1" }, "engines": { "node": ">=20.19.0" From 3d535f53880fe519a24ff4605f009a745cb25e3f Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:51:18 +0530 Subject: [PATCH 08/25] test(engine): independently verify 138 routes, safe Markdown, Unicode anchors, SEO, local search and interactive widgets --- scripts/scarp-site/test.ts | 122 +++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 scripts/scarp-site/test.ts diff --git a/scripts/scarp-site/test.ts b/scripts/scarp-site/test.ts new file mode 100644 index 0000000..513d79b --- /dev/null +++ b/scripts/scarp-site/test.ts @@ -0,0 +1,122 @@ +import assert from 'node:assert/strict' +import { existsSync, readFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { buildSite, previewRoot } from './build.ts' +import { renderMarkdown, splitFrontMatter } from './markdown.ts' +import { escapeHtml, localLink, htmlPath, pairedRoutes, sourceRoute } from './routes.ts' +import { documentationPages } from '../lib/docs-pages.ts' +import { urlForPage } from '../../docs/.vitepress/site-meta.ts' +import { validateSitemap } from '../lib/sitemap-policy.ts' + +const root=resolve(fileURLToPath(new URL('../..',import.meta.url))) +const docs=join(root,'docs') +const site=join(previewRoot,'scarp-docs') +let assertions=0 +function check(condition:boolean,message:string):void{assert.ok(condition,message);assertions++} + +assert.equal(sourceRoute('index.md'),'/scarp-docs/') +assert.equal(sourceRoute('hi/index.md'),'/scarp-docs/hi/') +assert.equal(sourceRoute('examples/index.md'),'/scarp-docs/examples/') +assert.equal(sourceRoute('hi/examples/index.md'),'/scarp-docs/hi/examples/') +assert.equal(sourceRoute('guide/first-program.md'),'/scarp-docs/guide/first-program') +assert.equal(pairedRoutes('hi/guide/first-program.md').english, + 'https://scarp-lang.github.io/scarp-docs/guide/first-program') +assert.equal(htmlPath('hi/guide/first-program.md'),'hi/guide/first-program.html') +assert.equal(localLink('/guide/learning-path','index.md'),'/scarp-docs/guide/learning-path') +assert.equal(localLink('/hi/','index.md'),'/scarp-docs/hi/') +assert.equal(localLink('https://github.com/scarp-lang','index.md'), + 'https://github.com/scarp-lang') +assert.equal(localLink('javascript:alert(1)','index.md'),'#') +assert.equal(localLink('../guide/first-program.md','tutorials/test.md'), + '/scarp-docs/guide/first-program') +assert.equal(escapeHtml('', + '~~~scarp', + 'funcra sample() -> integer {', + ' // ', + ' return 42;', + '}', + '~~~', + '::: warning ध्यान दें', + 'User content must be parsed safely.', + ':::', + '' +].join('\n'),'guide/test.md') +check(fixture.headings[0].id==='परीक्षण-heading','Devanagari heading must retain Unicode marks') +check(fixture.headings[1].id==='duplicate-test','Heading slug must be stable') +check(fixture.headings[2].id==='duplicate-test-1','Duplicate headings must get distinct IDs') +check(fixture.html.includes('href="/scarp-docs/guide/first-program"'),'Internal links must use GitHub Pages base') +check(fixture.html.includes('rel="noopener noreferrer"'),'External links should be hardened') +check(!fixture.html.includes('