@@ -7,20 +7,48 @@ import { icons, ns } from 'solid-ui'
77import { Util } from 'rdflib'
88import { marked } from 'marked'
99import DOMPurify from 'dompurify'
10+ import type { DataBrowserContext } from 'pane-registry'
11+ import type { NamedNode } from 'rdflib'
1012import './humanReadablePane.css'
1113
1214// Helper function to check if a URI has a markdown file extension
13- const isMarkdownFile = ( uri ) => {
15+ type DokieliCacheValue = 'dokieli' | 'html'
16+
17+ type HumanReadableIcon = string | Promise < string >
18+
19+ type HumanReadablePaneDefinition = {
20+ icon : ( subject : NamedNode , context : DataBrowserContext ) => HumanReadableIcon
21+ name : string
22+ label : ( subject : NamedNode , context : DataBrowserContext ) => 'view' | 'View' | null
23+ render : ( subject : NamedNode , context : DataBrowserContext ) => HTMLDivElement
24+ }
25+
26+ type RenderEnvironmentLike = {
27+ layout ?: 'mobile' | 'desktop' | string
28+ }
29+
30+ const isMarkdownFile = ( uri ?: string | null ) : boolean => {
1431 if ( ! uri ) return false
1532 const path = uri . split ( '?' ) [ 0 ] . split ( '#' ) [ 0 ] // Remove query string and fragment
1633 return / \. ( m d | m a r k d o w n | m d o w n | m k d | m k d n ) $ / i. test ( path )
1734}
1835
1936// Cache for dokieli detection results (keyed by subject URI)
20- const dokieliCache = new Map ( )
37+ const dokieliCache = new Map < string , DokieliCacheValue > ( )
38+
39+ function applyFrameClasses (
40+ frame : HTMLElement ,
41+ modifier : 'markdown' | 'plain-text' | 'iframe' ,
42+ lines : number
43+ ) : void {
44+ frame . className = ''
45+ frame . classList . add ( 'human-readable-pane__frame' )
46+ frame . classList . add ( `human-readable-pane__frame--${ modifier } ` )
47+ frame . style . setProperty ( '--human-readable-pane-height' , `${ lines } em` )
48+ }
2149
22- const humanReadablePane = {
23- icon : function ( subject , context ) {
50+ const humanReadablePane : HumanReadablePaneDefinition = {
51+ icon : function ( subject : NamedNode , context : DataBrowserContext ) : HumanReadableIcon {
2452 // Markdown files detected by extension
2553 if ( subject && isMarkdownFile ( subject . uri ) ) {
2654 return icons . iconBase + 'markdown.svg'
@@ -78,7 +106,10 @@ const humanReadablePane = {
78106
79107 name : 'humanReadable' ,
80108
81- label : function ( subject , context ) {
109+ label : function (
110+ subject : NamedNode ,
111+ context : DataBrowserContext
112+ ) : 'view' | 'View' | null {
82113 const kb = context . session . store
83114
84115 // See also the source pane, which has lower precedence.
@@ -94,7 +125,11 @@ const humanReadablePane = {
94125 'video/mp4'
95126 ]
96127
97- const hasContentTypeIn = function ( kb , x , displayables ) {
128+ const hasContentTypeIn = function (
129+ kb : typeof context . session . store ,
130+ x : NamedNode ,
131+ displayables : string [ ]
132+ ) : boolean {
98133 const cts = kb . fetcher . getHeader ( x , 'content-type' )
99134 if ( cts ) {
100135 for ( let j = 0 ; j < cts . length ; j ++ ) {
@@ -109,7 +144,11 @@ const humanReadablePane = {
109144 }
110145
111146 // This data could come from a fetch OR from ldp container
112- const hasContentTypeIn2 = function ( kb , x , displayables ) {
147+ const hasContentTypeIn2 = function (
148+ kb : typeof context . session . store ,
149+ x : NamedNode ,
150+ displayables : string [ ]
151+ ) : boolean {
113152 const t = kb . findTypeURIs ( x )
114153 for ( let k = 0 ; k < displayables . length ; k ++ ) {
115154 if ( Util . mediaTypeClass ( displayables [ k ] ) . uri in t ) {
@@ -157,11 +196,19 @@ const humanReadablePane = {
157196 return null
158197 } ,
159198
160- render : function ( subject , context ) {
199+ render : function (
200+ subject : NamedNode ,
201+ context : DataBrowserContext
202+ ) : HTMLDivElement {
161203 const myDocument = context . dom
162204 const div = myDocument . createElement ( 'div' )
163205 const kb = context . session . store
164206
207+ function applyEnvironmentAttributes ( element : HTMLDivElement ) : void {
208+ const environment = ( context . environment ?? { } ) as RenderEnvironmentLike
209+ element . dataset . layout = environment . layout ?? 'desktop'
210+ }
211+
165212 const cts = kb . fetcher . getHeader ( subject . doc ( ) , 'content-type' )
166213 const ct = cts ? cts [ 0 ] . split ( ';' , 1 ) [ 0 ] . trim ( ) : null // remove content-type parameters
167214
@@ -176,64 +223,61 @@ const humanReadablePane = {
176223 }
177224
178225 // @@ When we can, use CSP to turn off scripts within the iframe
179- div . setAttribute ( 'class' , 'docView ')
180- div . setAttribute ( 'style' , 'display: block; width: 100%; max-width: 100%; box-sizing: border-box;' )
226+ div . classList . add ( 'human-readable-pane ')
227+ applyEnvironmentAttributes ( div )
181228
182229 // render markdown to html in a DIV element
183- const renderMarkdownContent = function ( frame ) {
230+ const renderMarkdownContent = function ( frame : HTMLDivElement ) {
184231 kb . fetcher . webOperation ( 'GET' , subject . uri ) . then ( response => {
185- const markdownText = response . responseText
232+ const markdownText = response . responseText ?? ''
186233 const lines = Math . min ( 30 , markdownText . split ( / \n / ) . length + 5 )
187- const res = marked . parse ( markdownText )
234+ const res = marked . parse ( markdownText , { async : false } )
188235 const clean = DOMPurify . sanitize ( res )
189236 frame . innerHTML = clean
190- frame . setAttribute ( 'class' , 'doc' )
191- frame . setAttribute ( 'style' , `display: block; border: 1px solid; padding: 1em; height: ${ lines } em; max-width: 100%; width: 100%; box-sizing: border-box; resize: both; overflow: auto;` )
237+ applyFrameClasses ( frame , 'markdown' , lines )
192238 } ) . catch ( error => {
193239 console . error ( 'Error fetching markdown content:' , error )
194240 frame . innerHTML = '<p>Error loading content</p>'
195241 } )
196242 }
197243
198244 // render plain text in a PRE element
199- const renderPlainTextContent = function ( frame ) {
245+ const renderPlainTextContent = function ( frame : HTMLPreElement ) {
200246 kb . fetcher . webOperation ( 'GET' , subject . uri ) . then ( response => {
201- const plainText = response . responseText
247+ const plainText = response . responseText ?? ''
202248 const lines = Math . min ( 30 , plainText . split ( / \n / ) . length + 5 )
203249 frame . textContent = plainText
204- frame . setAttribute ( 'class' , 'doc' )
205- frame . setAttribute ( 'style' , `display: block; border: 1px solid; padding: 1em; height: ${ lines } em; max-width: 100%; width: 100%; box-sizing: border-box; resize: both; overflow: auto; font-family: monospace; white-space: pre-wrap; word-wrap: break-word;` )
250+ applyFrameClasses ( frame , 'plain-text' , lines )
206251 } ) . catch ( error => {
207252 console . error ( 'Error fetching plain text content:' , error )
208253 frame . textContent = 'Error loading content'
209254 } )
210255 }
211256
212- const setIframeAttributes = ( frame , lines ) => {
257+ const setIframeAttributes = ( frame : HTMLIFrameElement , lines : number ) => {
213258 frame . setAttribute ( 'src' , subject . uri )
214- frame . setAttribute ( 'class' , 'doc' )
215- frame . setAttribute ( 'style' , `display: block; border: 1px solid; padding: 1em; height: ${ lines } em; max-width: 100%; width: 100%; box-sizing: border-box; resize: both; overflow: auto;` )
259+ applyFrameClasses ( frame , 'iframe' , lines )
216260 }
217261
218262 if ( isMarkdown ) {
219263 // For markdown, use a DIV element and render the content
220- const frame = myDocument . createElement ( 'DIV ' )
264+ const frame = myDocument . createElement ( 'div ' )
221265 renderMarkdownContent ( frame )
222266 const frameContainer = myDocument . createElement ( 'div' )
223- frameContainer . setAttribute ( 'style' , 'display: block; width: 100%; max-width: 100%; box-sizing: border-box; ')
267+ frameContainer . classList . add ( 'human-readable-pane__container ')
224268 frameContainer . appendChild ( frame )
225269 div . appendChild ( frameContainer )
226270 } else if ( isPlainText ) {
227271 // For plain text, use a PRE element and render the content
228- const frame = myDocument . createElement ( 'PRE ' )
272+ const frame = myDocument . createElement ( 'pre ' )
229273 renderPlainTextContent ( frame )
230274 const frameContainer = myDocument . createElement ( 'div' )
231- frameContainer . setAttribute ( 'style' , 'display: block; width: 100%; max-width: 100%; box-sizing: border-box; ')
275+ frameContainer . classList . add ( 'human-readable-pane__container ')
232276 frameContainer . appendChild ( frame )
233277 div . appendChild ( frameContainer )
234278 } else {
235279 // For other content types, use IFRAME
236- const frame = myDocument . createElement ( 'IFRAME ' )
280+ const frame = myDocument . createElement ( 'iframe ' )
237281
238282 // Apply sandbox for HTML/XHTML
239283 if ( ct === 'text/html' || ct === 'application/xhtml+xml' ) {
@@ -242,7 +286,7 @@ const humanReadablePane = {
242286
243287 // Fetch content to calculate lines dynamically
244288 kb . fetcher . webOperation ( 'GET' , subject . uri ) . then ( response => {
245- const blobText = response . responseText
289+ const blobText = response . responseText ?? ''
246290 const newLines = blobText . includes ( '<script src="https://dokie.li/scripts/dokieli.js">' ) ? - 10 : 5
247291 const lines = Math . min ( 30 , blobText . split ( / \n / ) . length + newLines )
248292
@@ -259,7 +303,7 @@ const humanReadablePane = {
259303 } )
260304
261305 const frameContainer = myDocument . createElement ( 'div' )
262- frameContainer . setAttribute ( 'style' , 'display: block; width: 100%; max-width: 100%; box-sizing: border-box; ')
306+ frameContainer . classList . add ( 'human-readable-pane__container ')
263307 frameContainer . appendChild ( frame )
264308 div . appendChild ( frameContainer )
265309 }
0 commit comments