11import { defineConfig , type DefaultTheme , type HeadConfig } from 'vitepress'
22import llmstxt from 'vitepress-plugin-llms'
3+ import { expressiveSamplePlugin } from './plugins/expressive-sample'
4+ import { readFileSync , existsSync } from 'fs'
5+ import { resolve , dirname } from 'path'
6+ import { fileURLToPath } from 'url'
7+ import { createHash } from 'crypto'
8+
9+ const __dirname = dirname ( fileURLToPath ( import . meta. url ) )
310
411const base = '/ExpressiveSharp/'
512
@@ -16,13 +23,20 @@ const sidebar: DefaultTheme.Sidebar = {
1623 {
1724 text : 'Core APIs' ,
1825 items : [
26+ { text : 'IExpressiveQueryable<T>' , link : '/guide/expressive-queryable' } ,
1927 { text : '[Expressive] Properties' , link : '/guide/expressive-properties' } ,
2028 { text : '[Expressive] Methods' , link : '/guide/expressive-methods' } ,
2129 { text : 'Extension Members' , link : '/guide/extension-members' } ,
2230 { text : 'Constructor Projections' , link : '/guide/expressive-constructors' } ,
2331 { text : 'ExpressionPolyfill.Create' , link : '/guide/expression-polyfill' } ,
24- { text : 'IExpressiveQueryable<T>' , link : '/guide/expressive-queryable' } ,
25- { text : 'EF Core Integration' , link : '/guide/ef-core-integration' } ,
32+ ]
33+ } ,
34+ {
35+ text : 'Integrations' ,
36+ items : [
37+ { text : 'EF Core' , link : '/guide/integrations/ef-core' } ,
38+ { text : 'MongoDB' , link : '/guide/integrations/mongodb' } ,
39+ { text : 'Custom Providers' , link : '/guide/integrations/custom-providers' } ,
2640 ]
2741 } ,
2842 {
@@ -99,11 +113,154 @@ const headers = process.env.GITHUB_ACTIONS === "true" ?
99113 [ ...baseHeaders , umamiScript ] :
100114 baseHeaders ;
101115
116+ // Vite plugin: serve _playground/app.htm as raw HTML in dev mode.
117+ // VitePress's dev server applies its SPA transform to all HTML files in
118+ // public/, which breaks the Blazor WASM app. This middleware intercepts
119+ // requests to _playground/app.htm and serves the raw file directly.
120+ const mimeTypes : Record < string , string > = {
121+ '.htm' : 'text/html' , '.html' : 'text/html' , '.js' : 'application/javascript' ,
122+ '.mjs' : 'application/javascript' , '.css' : 'text/css' , '.json' : 'application/json' ,
123+ '.wasm' : 'application/wasm' , '.dll' : 'application/octet-stream' ,
124+ '.dat' : 'application/octet-stream' , '.br' : 'application/octet-stream' ,
125+ '.gz' : 'application/octet-stream' , '.woff' : 'font/woff' , '.woff2' : 'font/woff2' ,
126+ }
127+
128+ // Expands `::: expressive-sample` containers into fenced code blocks for each
129+ // render target BEFORE VitePress or llmstxt sees the markdown. This way:
130+ // - llms.txt sees the actual SQL / MongoDB / generator output
131+ // - VitePress renders the fenced blocks as regular code blocks (with Shiki
132+ // highlighting) which our markdown-it plugin picks up and wraps as tabs
133+ // The fenced blocks are the single source of truth the Vue component reads
134+ // from via the `data-expressive-sample` marker injected on the first block.
135+ function expandExpressiveSamplesPlugin ( ) {
136+ return {
137+ name : 'expand-expressive-samples' ,
138+ enforce : 'pre' as const ,
139+ transform ( code : string , id : string ) {
140+ if ( ! id . endsWith ( '.md' ) ) return null
141+ if ( ! code . includes ( '::: expressive-sample' ) ) return null
142+
143+ const relPath = id . includes ( '/docs/' )
144+ ? id . substring ( id . indexOf ( '/docs/' ) + 6 ) . replace ( / \? .* $ / , '' )
145+ : id
146+ const jsonPath = resolve ( __dirname , 'data/samples' , relPath . replace ( / \. m d $ / , '.json' ) )
147+ if ( ! existsSync ( jsonPath ) ) return null
148+
149+ type Target = { label : string ; language : string ; output : string }
150+ type Sample = { key : string ; snippet : string ; setup ?: string | null ; targets : Record < string , Target > }
151+ let samples : Sample [ ]
152+ try { samples = JSON . parse ( readFileSync ( jsonPath , 'utf-8' ) ) } catch { return null }
153+
154+ const lines = code . split ( '\n' )
155+ const result : string [ ] = [ ]
156+ let i = 0
157+ while ( i < lines . length ) {
158+ if ( ! lines [ i ] . trimStart ( ) . startsWith ( '::: expressive-sample' ) ) {
159+ result . push ( lines [ i ] ) ; i ++ ; continue
160+ }
161+ i ++
162+ const bodyLines : string [ ] = [ ]
163+ while ( i < lines . length && lines [ i ] . trimStart ( ) !== ':::' ) {
164+ bodyLines . push ( lines [ i ] ) ; i ++
165+ }
166+ i ++ // closing :::
167+
168+ const body = bodyLines . join ( '\n' ) . trim ( )
169+ const sepIdx = body . indexOf ( '---setup---' )
170+ const snippet = sepIdx >= 0 ? body . slice ( 0 , sepIdx ) . trim ( ) : body
171+ const setup = sepIdx >= 0 ? body . slice ( sepIdx + '---setup---' . length ) . trim ( ) : undefined
172+
173+ const key = createHash ( 'sha256' )
174+ . update ( snippet + '\0' + ( setup ?? '' ) )
175+ . digest ( 'hex' ) . slice ( 0 , 12 ) . toLowerCase ( )
176+ const sample = samples . find ( s => s . key === key )
177+ if ( ! sample ) {
178+ // Fallback: leave the container for our markdown-it plugin's warning
179+ result . push ( '::: expressive-sample' )
180+ result . push ( ...bodyLines )
181+ result . push ( ':::' )
182+ continue
183+ }
184+
185+ // Preserve original container — our markdown-it plugin (VitePress
186+ // render stage) reads this and emits the interactive Vue tabs.
187+ result . push ( '::: expressive-sample' )
188+ result . push ( ...bodyLines )
189+ result . push ( ':::' )
190+
191+ // Also emit fenced code blocks inside a hidden div. These are invisible
192+ // on the rendered page (Vue component handles the UI) but are included
193+ // in the raw .md that llms.txt sees, so crawlers/LLMs get the full SQL
194+ // and pipeline output for each render target.
195+ result . push ( '' )
196+ result . push ( '<div class="expressive-sample-llms" style="display:none">' )
197+ result . push ( '' )
198+ // For LLMs: include C# input and ONE representative SQL output (SQLite).
199+ // The other providers are mostly SQL-dialect noise that doesn't teach
200+ // anything about ExpressiveSharp; the generator output is boilerplate
201+ // that shouldn't influence LLM suggestions toward [InterceptsLocation].
202+ let csharpContent = sample . snippet
203+ if ( sample . setup ) csharpContent += '\n\n// Setup\n' + sample . setup
204+ result . push ( '```csharp' )
205+ result . push ( csharpContent )
206+ result . push ( '```' )
207+ const sqlite = sample . targets [ 'sqlite' ]
208+ if ( sqlite ) {
209+ result . push ( '' )
210+ result . push ( `**Generated SQL:**` )
211+ result . push ( '' )
212+ result . push ( '```' + sqlite . language )
213+ result . push ( sqlite . output )
214+ result . push ( '```' )
215+ }
216+ result . push ( '' )
217+ result . push ( '</div>' )
218+ result . push ( '' )
219+ }
220+ return { code : result . join ( '\n' ) , map : null }
221+ }
222+ }
223+ }
224+
225+ function servePlaygroundPlugin ( ) {
226+ return {
227+ name : 'serve-playground' ,
228+ configureServer ( server : any ) {
229+ // Serve everything under /_playground/ as raw static files so VitePress's
230+ // SPA transform and module system don't intercept Blazor WASM resources.
231+ server . middlewares . use ( ( req : any , res : any , next : any ) => {
232+ const prefix = '/ExpressiveSharp/_playground/'
233+ if ( ! req . url ?. startsWith ( prefix ) ) return next ( )
234+
235+ const relPath = req . url . slice ( prefix . length ) . split ( '?' ) [ 0 ]
236+ const filePath = resolve ( __dirname , '../public/_playground' , relPath )
237+ if ( ! existsSync ( filePath ) ) return next ( )
238+
239+ const ext = '.' + relPath . split ( '.' ) . pop ( )
240+ res . setHeader ( 'Content-Type' , mimeTypes [ ext ] || 'application/octet-stream' )
241+ res . end ( readFileSync ( filePath ) )
242+ } )
243+ }
244+ }
245+ }
246+
102247export default defineConfig ( {
103248 title : "ExpressiveSharp" ,
104249 description : "Modern C# syntax in LINQ expression trees — source-generated at compile time" ,
105250 base,
106251 head : headers ,
252+ markdown : {
253+ config : ( md ) => {
254+ md . use ( expressiveSamplePlugin )
255+ }
256+ } ,
257+ vue : {
258+ template : {
259+ compilerOptions : {
260+ isCustomElement : ( tag ) => tag === 'expressive-playground' ,
261+ }
262+ }
263+ } ,
107264 themeConfig : {
108265 logo : '/logo.png' ,
109266 nav : [
@@ -112,6 +269,7 @@ export default defineConfig({
112269 { text : 'Reference' , link : '/reference/expressive-attribute' } ,
113270 { text : 'Advanced' , link : '/advanced/how-it-works' } ,
114271 { text : 'Recipes' , link : '/recipes/computed-properties' } ,
272+ { text : 'Playground' , link : '/playground-editor' } ,
115273 { text : 'Benchmarks' , link : 'https://efnext.github.io/ExpressiveSharp/dev/bench/' } ,
116274 ] ,
117275
@@ -132,6 +290,8 @@ export default defineConfig({
132290 } ,
133291 vite : {
134292 plugins : [
293+ expandExpressiveSamplesPlugin ( ) ,
294+ servePlaygroundPlugin ( ) ,
135295 llmstxt ( {
136296 domain : 'https://efnext.github.io' ,
137297 description : 'Modern C# syntax in LINQ expression trees — source-generated at compile time' ,
0 commit comments