@@ -2,6 +2,7 @@ import {dirname, join} from "node:path";
2
2
import { parseHTML } from "linkedom" ;
3
3
import { type Config , type Page , type Section } from "./config.js" ;
4
4
import { computeHash } from "./hash.js" ;
5
+ import { type Html , html } from "./html.js" ;
5
6
import { resolveImport } from "./javascript/imports.js" ;
6
7
import { type FileReference , type ImportReference } from "./javascript.js" ;
7
8
import { type CellPiece , type ParseResult , parseMarkdown } from "./markdown.js" ;
@@ -39,7 +40,7 @@ export function renderServerless(source: string, options: RenderOptions): Render
39
40
} ;
40
41
}
41
42
42
- export function renderDefineCell ( cell ) {
43
+ export function renderDefineCell ( cell ) : string {
43
44
const { id, inline, inputs, outputs, files, body, databases} = cell ;
44
45
return `define({${ Object . entries ( { id, inline, inputs, outputs, files, databases} )
45
46
. filter ( ( arg ) => arg [ 1 ] !== undefined )
@@ -55,79 +56,71 @@ function render(
55
56
parseResult : ParseResult ,
56
57
{ path, pages, title, toc, preview, hash, resolver} : RenderOptions & RenderInternalOptions
57
58
) : string {
58
- const table = tableOfContents ( parseResult , toc ) ;
59
- return `<!DOCTYPE html>
60
- <meta charset="utf-8">${ path === "/404" ? `\n<base href="/">` : "" }
59
+ const pageTocConfig = parseResult . data ?. toc ;
60
+ const tocLabel = pageTocConfig ?. label ?? toc ?. label ;
61
+ const tocHeaders = ( pageTocConfig ?. show ?? toc ?. show ) && findHeaders ( parseResult ) ;
62
+ return String ( html `<!DOCTYPE html>
63
+ < meta charset ="utf-8 "> ${ path === "/404" ? html `\n< base href ="/ "> ` : "" }
61
64
< meta name ="viewport " content ="width=device-width, initial-scale=1, maximum-scale=1 ">
62
65
${
63
66
parseResult . title || title
64
- ? `<title>${ [ parseResult . title , parseResult . title === title ? null : title ]
67
+ ? html `< title > ${ [ parseResult . title , parseResult . title === title ? null : title ]
65
68
. filter ( ( title ) : title is string => ! ! title )
66
- . map ( ( title ) => escapeData ( title ) )
67
69
. join ( " | " ) } </ title > \n`
68
70
: ""
69
71
} < link rel ="preconnect " href ="https://fonts.gstatic.com " crossorigin >
70
72
< link rel ="stylesheet " type ="text/css " href ="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap ">
71
- <link rel="stylesheet" type="text/css" href="${ escapeDoubleQuoted ( relativeUrl ( path , "/_observablehq/style.css" ) ) } ">
72
- ${ Array . from ( getImportPreloads ( parseResult , path ) )
73
- . map ( ( href ) => ` <link rel="modulepreload" href="${ escapeDoubleQuoted ( relativeUrl ( path , href ) ) } ">`)
74
- . join ( "\n" ) }
75
- <script type="module">
73
+ < link rel ="stylesheet " type ="text/css " href ="${ relativeUrl ( path , "/_observablehq/style.css" ) } "> ${ Array . from (
74
+ getImportPreloads ( parseResult , path ) ,
75
+ ( href ) => html `\n < link rel ="modulepreload " href ="${ relativeUrl ( path , href ) } "> `
76
+ ) }
77
+ < script type ="module "> ${ html . unsafe ( `
76
78
77
79
import {${ preview ? "open, " : "" } define} from ${ JSON . stringify ( relativeUrl ( path , "/_observablehq/client.js" ) ) } ;
78
80
79
81
${ preview ? `open({hash: ${ JSON . stringify ( hash ) } , eval: (body) => (0, eval)(body)});\n` : "" } ${ parseResult . cells
80
82
. map ( resolver )
81
83
. map ( renderDefineCell )
82
- . join ( "" ) }
84
+ . join ( "" ) } ` ) }
83
85
</ script >
84
86
${ pages . length > 0 ? sidebar ( title , pages , path ) : "" }
85
- ${ table } <div id="observablehq-center">
87
+ ${ tocHeaders ?. length > 0 ? tableOfContents ( tocHeaders , tocLabel ) : "" } < div id ="observablehq-center ">
86
88
< main id ="observablehq-main " class ="observablehq ">
87
- ${ parseResult . html } </main>
89
+ ${ html . unsafe ( parseResult . html ) } </ main >
88
90
${ footer ( path , { pages, title} ) }
89
91
</ div >
90
- ` ;
92
+ ` ) ;
91
93
}
92
94
93
- function sidebar ( title : string | undefined , pages : ( Page | Section ) [ ] , path : string ) : string {
94
- return `<input id="observablehq-sidebar-toggle" type="checkbox">
95
+ function sidebar ( title : string | undefined , pages : ( Page | Section ) [ ] , path : string ) : Html {
96
+ return html `< input id ="observablehq-sidebar-toggle " type ="checkbox ">
95
97
< nav id ="observablehq-sidebar ">
96
98
< ol >
97
- <li class="observablehq-link${ path === "/index" ? " observablehq-link-active" : "" } "><a href="${ escapeDoubleQuoted (
98
- relativeUrl ( path , "/" )
99
- ) } ">${ escapeData ( title ?? "Home" ) } </a></li>
99
+ < li class ="observablehq-link ${ path === "/index" ? " observablehq-link-active" : "" } "> < a href ="${ relativeUrl (
100
+ path ,
101
+ "/"
102
+ ) } "> ${ title ?? "Home" } </ a > </ li >
100
103
</ ol >
101
- <ol>${ pages
102
- . map ( ( p , i ) =>
103
- "pages" in p
104
- ? `${ i > 0 && "path" in pages [ i - 1 ] ? "</ol>" : "" }
104
+ < ol > ${ pages . map ( ( p , i ) =>
105
+ "pages" in p
106
+ ? html `${ i > 0 && "path" in pages [ i - 1 ] ? html `</ ol > ` : "" }
105
107
< details ${ p . open === undefined || p . open ? " open" : "" } >
106
- <summary>${ escapeData ( p . name ) } </summary>
107
- <ol>${ p . pages
108
- . map (
109
- ( p ) => `
110
- ${ renderListItem ( p , path ) } `
111
- )
112
- . join ( "" ) }
108
+ < summary > ${ p . name } </ summary >
109
+ < ol > ${ p . pages . map ( ( p ) => renderListItem ( p , path ) ) }
113
110
</ ol >
114
111
</ details > `
115
- : "path" in p
116
- ? `${
117
- i === 0
118
- ? `
119
- `
120
- : ! ( "path" in pages [ i - 1 ] )
121
- ? `
112
+ : "path" in p
113
+ ? html `${
114
+ i === 0
115
+ ? ""
116
+ : ! ( "path" in pages [ i - 1 ] )
117
+ ? html `
122
118
</ ol >
123
- <ol>
124
- `
125
- : `
126
- `
127
- } ${ renderListItem ( p , path ) } `
128
- : null
129
- )
130
- . join ( "" ) }
119
+ < ol > `
120
+ : ""
121
+ } ${ renderListItem ( p , path ) } `
122
+ : ""
123
+ ) }
131
124
</ ol >
132
125
</ nav >
133
126
< script > {
@@ -138,38 +131,34 @@ function sidebar(title: string | undefined, pages: (Page | Section)[], path: str
138
131
} </ script > `;
139
132
}
140
133
141
- function tableOfContents ( parseResult : ParseResult , toc : RenderOptions [ "toc" ] ) {
142
- const pageTocConfig = parseResult . data ?. toc ;
143
- const headers =
144
- ( pageTocConfig ?. show ?? toc ?. show ) &&
145
- Array . from ( parseHTML ( parseResult . html ) . document . querySelectorAll ( "h2" ) )
146
- . map ( ( node ) => ( {
147
- label : node . textContent ,
148
- href : node . firstElementChild ?. getAttribute ( "href" )
149
- } ) )
150
- . filter ( ( d ) => d . label && d . href ) ;
151
- return headers ?. length
152
- ? `<aside id="observablehq-toc">
134
+ interface Header {
135
+ label : string ;
136
+ href : string ;
137
+ }
138
+
139
+ function findHeaders ( parseResult : ParseResult ) : Header [ ] {
140
+ return Array . from ( parseHTML ( parseResult . html ) . document . querySelectorAll ( "h2" ) )
141
+ . map ( ( node ) => ( { label : node . textContent , href : node . firstElementChild ?. getAttribute ( "href" ) } ) )
142
+ . filter ( ( d ) : d is Header => ! ! d . label && ! ! d . href ) ;
143
+ }
144
+
145
+ function tableOfContents ( headers : Header [ ] , label = "Contents" ) : Html {
146
+ return html `< aside id ="observablehq-toc ">
153
147
< nav >
154
- <div>${ escapeData ( pageTocConfig ?. label ?? toc ?. label ?? "Contents" ) } </div>
155
- <ol>
156
- ${ headers
157
- . map (
158
- ( { label, href} ) =>
159
- `<li class="observablehq-secondary-link"><a href="${ escapeDoubleQuoted ( href ) } ">${ escapeData ( label ) } </a></li>`
160
- )
161
- . join ( "\n" ) }
148
+ < div > ${ label } </ div >
149
+ < ol > ${ headers . map (
150
+ ( { label, href} ) => html `\n< li class ="observablehq-secondary-link "> < a href ="${ href } "> ${ label } </ a > </ li > `
151
+ ) }
162
152
</ ol >
163
153
</ nav >
164
154
</ aside >
165
- `
166
- : "" ;
155
+ ` ;
167
156
}
168
157
169
- function renderListItem ( p : Page , path : string ) : string {
170
- return ` <li class="observablehq-link${
158
+ function renderListItem ( p : Page , path : string ) : Html {
159
+ return html `\n < li class ="observablehq-link ${
171
160
p . path === path ? " observablehq-link-active" : ""
172
- } "><a href="${ escapeDoubleQuoted ( relativeUrl ( path , prettyPath ( p . path ) ) ) } ">${ escapeData ( p . name ) } </a></li>`;
161
+ } "> < a href ="${ relativeUrl ( path , prettyPath ( p . path ) ) } "> ${ p . name } </ a > </ li > `;
173
162
}
174
163
175
164
function prettyPath ( path : string ) : string {
@@ -203,34 +192,18 @@ function getImportPreloads(parseResult: ParseResult, path: string): Iterable<str
203
192
return preloads ;
204
193
}
205
194
206
- // TODO Adopt Hypertext Literal?
207
- function escapeDoubleQuoted ( value ) : string {
208
- return `${ value } ` . replace ( / [ " & ] / g, entity ) ;
209
- }
210
-
211
- // TODO Adopt Hypertext Literal?
212
- function escapeData ( value : string ) : string {
213
- return `${ value } ` . replace ( / [ < & ] / g, entity ) ;
214
- }
215
-
216
- function entity ( character ) {
217
- return `&#${ character . charCodeAt ( 0 ) . toString ( ) } ;` ;
218
- }
219
-
220
- function footer ( path : string , options ?: Pick < Config , "pages" | "title" > ) : string {
195
+ function footer ( path : string , options ?: Pick < Config , "pages" | "title" > ) : Html {
221
196
const link = pager ( path , options ) ;
222
- return `<footer id="observablehq-footer">\n${
223
- link ? `${ pagenav ( path , link ) } \n` : ""
197
+ return html `< footer id ="observablehq-footer "> \n${
198
+ link ? html `${ pagenav ( path , link ) } \n` : ""
224
199
} < div > © ${ new Date ( ) . getUTCFullYear ( ) } Observable, Inc.</ div >
225
200
</ footer > `;
226
201
}
227
202
228
- function pagenav ( path : string , { prev, next} : PageLink ) : string {
229
- return `<nav>${ prev ? pagelink ( path , prev , "prev" ) : "" } ${ next ? pagelink ( path , next , "next" ) : "" } </nav>` ;
203
+ function pagenav ( path : string , { prev, next} : PageLink ) : Html {
204
+ return html `< nav > ${ prev ? pagelink ( path , prev , "prev" ) : "" } ${ next ? pagelink ( path , next , "next" ) : "" } </ nav > ` ;
230
205
}
231
206
232
- function pagelink ( path : string , page : Page , rel : "prev" | "next" ) : string {
233
- return `<a rel="${ escapeDoubleQuoted ( rel ) } " href="${ escapeDoubleQuoted (
234
- relativeUrl ( path , prettyPath ( page . path ) )
235
- ) } "><span>${ escapeData ( page . name ) } </span></a>`;
207
+ function pagelink ( path : string , page : Page , rel : "prev" | "next" ) : Html {
208
+ return html `< a rel ="${ rel } " href ="${ relativeUrl ( path , prettyPath ( page . path ) ) } "> < span > ${ page . name } </ span > </ a > ` ;
236
209
}
0 commit comments