@@ -99,6 +99,48 @@ function appJSONLoadedHandler() {
9999 } ) ;
100100}
101101
102+ /**
103+ * Extract an app name from a /apps/appname path
104+ * - assumes app names cannot contain periods
105+ * - assumes apps cannot be named "apps"
106+ * - assumes we're in or including the "apps" folder in the href
107+ * Returns the app name string or null if not an app folder.
108+ */
109+ function extractAppNameFromHref ( href ) {
110+ if ( ! href ) return null ;
111+
112+ try {
113+ const u = new URL ( href ) ;
114+ href = u . pathname ;
115+ } catch ( e ) {
116+ // ignore - just use href as-is
117+ }
118+ // very unlikely, but get rid of query/hash
119+ href = href . split ( '?' ) [ 0 ] . split ( '#' ) [ 0 ] . trim ( ) ;
120+ // remove leading/trailing slashes
121+ href = href . replace ( / ^ \/ + | \/ + $ / g, '' ) ;
122+ if ( ! href ) return null ; // was just /, throw it out
123+
124+ const parts = href . split ( '/' ) . filter ( Boolean ) ;
125+ if ( parts . length === 0 ) return null ;
126+ // allow './' prefixes by dropping leading '.' segments
127+ while ( parts . length && parts [ 0 ] === '.' ) parts . shift ( ) ;
128+ if ( parts . length === 0 ) return null ; // skip if it was current dir only
129+ // reject any parent-directory references anywhere
130+ if ( parts . some ( p => p === '..' ) ) return null ;
131+ // prefer an 'apps' segment anywhere in the path; otherwise use first folder
132+ const appsIdx = parts . findIndex ( p => p . toLowerCase ( ) === 'apps' ) ;
133+ let candidate ;
134+ if ( appsIdx >= 0 && appsIdx + 1 < parts . length ) candidate = parts [ appsIdx + 1 ] ;
135+ else candidate = parts [ 0 ] ;
136+ if ( ! candidate ) return null ;
137+ // if the only thing we found is 'apps', ignore it
138+ if ( candidate . toLowerCase ( ) === 'apps' ) return null ;
139+ // skip names with periods
140+ if ( candidate . includes ( '.' ) ) return null ;
141+ return candidate ;
142+ }
143+
102144httpGet ( Const . APPS_JSON_FILE ) . then ( apps => {
103145 if ( apps . startsWith ( "---" ) ) {
104146 showToast ( Const . APPS_JSON_FILE + " still contains Jekyll markup" , "warning" ) ;
@@ -127,8 +169,12 @@ httpGet(Const.APPS_JSON_FILE).then(apps=>{
127169 let promises = [ ] ;
128170 htmlToArray ( xmlDoc . querySelectorAll ( "a" ) ) . forEach ( a => {
129171 let href = a . getAttribute ( "href" ) ;
130- if ( ! href || href . startsWith ( "/" ) || href . startsWith ( "_" ) || ! href . endsWith ( "/" ) ) return ;
131- let metadataURL = appsURL + "/" + href + "metadata.json" ;
172+ const appName = extractAppNameFromHref ( href ) ;
173+ // Skip anything that doesn't look like an app or is an _example_app
174+ if ( ! appName || appName . startsWith ( "_" ) ) {
175+ return ;
176+ }
177+ let metadataURL = appsURL + appName + "/metadata.json" ;
132178 console . log ( " - Loading " + metadataURL ) ;
133179 promises . push ( httpGet ( metadataURL ) . then ( metadataText => {
134180 try {
0 commit comments