`;
+ }
+
+ this.contentLoading = false;
+
+ this.$nextTick( () => {
+ this.scrollContentTop();
+ // Re-run syntax highlighting on the newly injected content
+ if ( window.SyntaxHighlighter ) {
+ SyntaxHighlighter.config.stripBrs = true;
+ SyntaxHighlighter.defaults.gutter = false;
+ SyntaxHighlighter.defaults.toolbar = false;
+ SyntaxHighlighter.highlight();
+ }
+ } );
+ },
+
+ // ── Sidebar ──────────────────────────────────────────────────────────
+
+ // Recursively count all commands under a namespace at any depth.
+ countCommands( ns ) {
+ let n = ( ns.commands || [] ).length;
+ for ( const ch of ns.children || [] ) n += this.countCommands( ch );
+ return n;
+ },
+
+ toggleNamespace( key ) {
+ const idx = this.expandedNamespaces.indexOf( key );
+ if ( idx > -1 ) {
+ this.expandedNamespaces.splice( idx, 1 );
+ } else {
+ this.expandedNamespaces.push( key );
+ }
+ },
+
+ isExpanded( key ) {
+ return this.expandedNamespaces.includes( key );
+ },
+
+ // ── Tree keyboard navigation (ARIA tree pattern) ──────────────────────
+ treeKeydown( event ) {
+ const arrowKeys = [ "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End" ];
+ if ( !arrowKeys.includes( event.key ) ) return;
+ event.preventDefault();
+
+ const tree = event.currentTarget;
+ const items = Array.from( tree.querySelectorAll( "[role='treeitem']" ) );
+ const focused = document.activeElement;
+ const idx = items.indexOf( focused );
+
+ if ( event.key === "ArrowDown" ) {
+ if ( idx < items.length - 1 ) items[ idx + 1 ].focus();
+ } else if ( event.key === "ArrowUp" ) {
+ if ( idx > 0 ) items[ idx - 1 ].focus();
+ } else if ( event.key === "Home" ) {
+ items[ 0 ]?.focus();
+ } else if ( event.key === "End" ) {
+ items[ items.length - 1 ]?.focus();
+ } else if ( event.key === "ArrowRight" ) {
+ // Expand collapsed namespace, or move to first child if already expanded
+ if ( focused?.getAttribute( "aria-expanded" ) === "false" ) {
+ focused.click();
+ } else if ( idx < items.length - 1 ) {
+ items[ idx + 1 ].focus();
+ }
+ } else if ( event.key === "ArrowLeft" ) {
+ // Collapse expanded namespace, or move focus to parent namespace
+ if ( focused?.getAttribute( "aria-expanded" ) === "true" ) {
+ focused.click();
+ } else {
+ const level = parseInt( focused?.getAttribute( "aria-level" ) || "1", 10 );
+ for ( let i = idx - 1; i >= 0; i-- ) {
+ if ( parseInt( items[ i ].getAttribute( "aria-level" ) || "1", 10 ) < level ) {
+ items[ i ].focus();
+ break;
+ }
+ }
+ }
+ }
+ },
+
+ // ── Search ────────────────────────────────────────────────────────────
+ performSearch() {
+ if ( !this.searchQuery.trim() ) {
+ this.searchResults = [];
+ this.selectedSearchIndex = 0;
+ return;
+ }
+ const q = this.searchQuery.toLowerCase();
+ this.searchResults = this.allCommands
+ .filter( c =>
+ c.command.toLowerCase().includes( q ) ||
+ c.searchList.toLowerCase().includes( q ) ||
+ ( c.hint && c.hint.toLowerCase().includes( q ) )
+ )
+ .slice( 0, 12 );
+ this.selectedSearchIndex = 0;
+ },
+
+ navigateSearch( direction ) {
+ if ( !this.searchResults.length ) return;
+ this.selectedSearchIndex += direction;
+ if ( this.selectedSearchIndex < 0 ) {
+ this.selectedSearchIndex = this.searchResults.length - 1;
+ } else if ( this.selectedSearchIndex >= this.searchResults.length ) {
+ this.selectedSearchIndex = 0;
+ }
+ },
+
+ selectSearchResult() {
+ if ( this.searchResults.length > 0 ) {
+ this.loadCommand( this.searchResults[ this.selectedSearchIndex ] );
+ this.searchQuery = "";
+ this.searchResults = [];
+ }
+ },
+
+ // ── Utilities ────────────────────────────────────────────────────────
+ scrollContentTop() {
+ const main = document.getElementById( "main-content" );
+ if ( main ) main.scrollTop = 0;
+ },
+
+ // Escape a string for safe insertion into HTML to prevent XSS
+ escapeHtml( str ) {
+ if ( typeof str !== "string" ) return "";
+ const div = document.createElement( "div" );
+ div.textContent = str;
+ return div.innerHTML;
+ },
+
+ // ── Computed ─────────────────────────────────────────────────────────
+
+ /**
+ * Flattens the namespace tree into a depth-annotated list for the sidebar.
+ * Only items whose ancestors are currently expanded are included, so
+ * visibility is driven by list membership rather than x-show.
+ * commandFilter is applied recursively at every depth.
+ *
+ * Each item is one of:
+ * { type: "ns", depth, key, ns } — namespace header (folder button)
+ * { type: "cmd", depth, cmd } — command link
+ */
+ get flatSidebarItems() {
+ const items = [];
+ const f = this.commandFilter.toLowerCase().trim();
+
+ const matchesFilter = ( ns ) => {
+ if ( !f ) return true;
+ if ( ns.name.toLowerCase().includes( f ) ) return true;
+ if ( ( ns.commands || [] ).some( c => c.command.toLowerCase().includes( f ) ) ) return true;
+ return ( ns.children || [] ).some( ch => matchesFilter( ch ) );
+ };
+
+ const flatten = ( nsList, depth, parentKey ) => {
+ for ( const ns of nsList ) {
+ if ( !matchesFilter( ns ) ) continue;
+ const key = parentKey ? parentKey + "/" + ns.name : ns.name;
+ items.push( { type: "ns", depth, key, ns } );
+ if ( this.isExpanded( key ) ) {
+ const visibleCmds = !f
+ ? ( ns.commands || [] )
+ : ( ns.commands || [] ).filter( c => c.command.toLowerCase().includes( f ) );
+ for ( const cmd of visibleCmds ) {
+ items.push( { type: "cmd", depth: depth + 1, cmd } );
+ }
+ if ( ns.children && ns.children.length ) {
+ flatten( ns.children, depth + 1, key );
+ }
+ }
+ }
+ };
+
+ flatten( this.namespaces, 0, "" );
+ return items;
+ },
+
+ get totalCommandCount() {
+ return this.allCommands.length;
+ },
+
+ // Root namespace object for the current command (first word of namespace)
+ get currentCommandParentNs() {
+ if ( !this.currentCommand?.namespace ) return null;
+ const parentName = this.currentCommand.namespace.split( " " )[ 0 ];
+ return this.namespaces.find( n => n.name === parentName ) || null;
+ },
+
+ // Deepest child namespace object for the current command.
+ // For "server java sub", walks namespaces → server → children → java → children → sub.
+ get currentCommandChildNs() {
+ if ( !this.currentCommand?.namespace ) return null;
+ const parts = this.currentCommand.namespace.split( " " );
+ if ( parts.length < 2 ) return null;
+ let node = this.namespaces.find( n => n.name === parts[ 0 ] );
+ if ( !node ) return null;
+ for ( let i = 1; i < parts.length; i++ ) {
+ const next = ( node.children || [] ).find( c => c.name === parts[ i ] );
+ if ( !next ) return null;
+ node = next;
+ }
+ return node;
+ },
+
+ get totalNamespaceCount() {
+ return this.namespaces.length;
+ }
+ };
+}
diff --git a/strategy/CommandBox/resources/templates/class.cfm b/strategy/CommandBox/themes/default/resources/templates/class.cfm
similarity index 66%
rename from strategy/CommandBox/resources/templates/class.cfm
rename to strategy/CommandBox/themes/default/resources/templates/class.cfm
index 90b4380..8ecd66f 100644
--- a/strategy/CommandBox/resources/templates/class.cfm
+++ b/strategy/CommandBox/themes/default/resources/templates/class.cfm
@@ -9,27 +9,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -42,46 +21,52 @@
>
-
#arguments.command#
-
-
-
-
-
- Aliases:
-
-
- #local.alias#
-
-
-
+
+
+
+
+ #arguments.command#
+
+
+
+
+
+ Aliases:
+
+
+ #local.alias#
+
+
+
-
-
+
-
- // All we care about is the "run()" method
- local.qFunctions = buildFunctionMetaData( arguments.metadata );
- local.qFunctions = getMetaSubQuery(local.qFunctions, "UPPER(name)='RUN'");
-
+
+ // All we care about is the "run()" method
+ local.qFunctions = buildFunctionMetaData( arguments.metadata );
+ local.qFunctions = getMetaSubQuery(local.qFunctions, "UPPER(name)='RUN'");
+
-
-
-
-
-
+
+
+
+
+
-
-