[Tool] Custom JSMin for mod_harbour v2.1 - Minify JS while protecting server tags
Hello everyone,
When developing web applications with mod_harbour v2.1, we often use a mixed-code architecture—embedding Harbour scripts ( <?prg ... ?> ) or PHP code directly inside frontend JavaScript files. It is an incredibly powerful and intuitive approach.
However, a major problem arises during the deployment phase when we need to minify our JS files to improve page load times. Standard minification tools (like Terser or UglifyJS) cannot parse server-side tags like <?prg. They typically crash, throw syntax errors, or completely destroy the server-side logic blocks.
Previously, I had to manually cut out all <?prg blocks, run the remaining JS through a minifier, and paste the blocks back in. Doing this manually for 20-30 files per project is not only exhausting but highly prone to errors.
To solve this, I decided to utilize the classic C-based tool written by JSON creator Douglas Crockford: JSMin
(Official GitHub source: https://github.com/douglascrockford/JSMin).
※ Important Development Note:
If you download the official jsmin.c and attempt to compile it alongside Harbour (e.g., hbmk2 jsmin_tool.prg jsmin.c), it will compile but will freeze upon execution!
This happens because the original C file contains a standard main() function, which hijacks Harbour's entry point. Furthermore, the original code only supports stdin/stdout and cannot communicate with Harbour variables.
Therefore, I leveraged Harbour's excellent #pragma BEGINDUMP mechanism. I modified the JSMin source by removing the main() function and adapting it to read/write memory buffers. The result is a fully encapsulated, single-file CLI tool explicitly designed for our mixed-code workflow!
Key Features:
Tag Protection Engine: Automatically detects and preserves <?prg ... ?> or <?php ... ?> blocks intact, applying comment/whitespace removal ONLY to the pure JavaScript sections.
Batch File Processing: Supports wildcard inputs (e.g., .js.prg).
In-place Overwrite: Use the -ow flag to overwrite files directly, integrating seamlessly into automated build scripts.
UTF-8 Safety: Built-in HB_CDPSELECT("UTF8") ensures that double-byte characters (like Traditional Chinese or other Unicode strings) do not conflict with ASCII escape characters during JSMin processing. (Note: Highly recommended to save your source files as UTF-8 without BOM).
Compilation & Usage:
Since the C core is fully encapsulated within, just save the source code below as jsmin_tool.prg and compile it as a single file:
hbmk2 jsmin_tool.prgDaily Deployment Commands:
:: Safe mode (generates a .min file automatically)
jsmin_tool -f cem.js.prg
:: Batch process and overwrite (perfect for deployment automation)
jsmin_tool -f *.js.prg -ow
:: Specify custom protection tags (e.g., for mixed PHP files)
jsmin_tool -f *.js.php -tb <?php -te ?> -owBonus Tip: Minifying Pure PHP Files Safely
If you also need to minify "pure PHP files" (stripping comments and whitespaces to protect your code logic), you don't need to write a custom tool for this!
PHP officially includes a built-in, completely safe method for minification. Simply use the -w parameter in your command line:
php.exe -w source.php > output.min.phpThis guarantees that complex strings and Heredoc syntaxes remain absolutely unbroken. I highly recommend combining this native command with the jsmin_tool above in your automated build batch scripts.
Complete Harbour Source Code:
/*
* jsmin_tool.prg
* JSMin tool supporting CLI parameters, multiple tags, wildcard files, and overwrite.
*/
FUNCTION Main( ... )
LOCAL aArgs := hb_aParams()
LOCAL i, cArg
LOCAL cFileMask := ""
LOCAL cSourceStr := ""
LOCAL aTags := {}
LOCAL cTmpTb := ""
LOCAL aFiles, cFile, cResult, cPath, cOutFile
LOCAL lOverwrite := .F.
HB_CDPSELECT("UTF8")
IF Len( aArgs ) == 0
? "=========================================================="
? " JSMin Tool with Multi-Tag Protection (Powered by Harbour)"
? "=========================================================="
? "Usage: jsmin_tool [options]"
? "Options:"
? " -f <file_mask> Process files (e.g., *.js.prg)"
? " -tb <start_tag> Set start tag for protected block"
? " -te <end_tag> Set end tag for protected block"
? " -ow Overwrite original files"
RETURN NIL
ENDIF
i := 1
DO WHILE i <= Len( aArgs )
cArg := lower( aArgs[i] )
IF Left( cArg, 3 ) == "-f="
cFileMask := SubStr( aArgs[i], 4 )
ELSEIF cArg == "-f" .AND. i < Len( aArgs )
i++; cFileMask := aArgs[i]
ELSEIF Left( cArg, 4 ) == "-tb="
cTmpTb := SubStr( aArgs[i], 5 )
ELSEIF cArg == "-tb" .AND. i < Len( aArgs )
i++; cTmpTb := aArgs[i]
ELSEIF Left( cArg, 4 ) == "-te="
IF !Empty( cTmpTb )
AAdd( aTags, { cTmpTb, SubStr( aArgs[i], 5 ) } )
cTmpTb := ""
ENDIF
ELSEIF cArg == "-te" .AND. i < Len( aArgs )
i++
IF !Empty( cTmpTb )
AAdd( aTags, { cTmpTb, aArgs[i] } )
cTmpTb := ""
ENDIF
ELSEIF cArg == "-ow" .OR. cArg == "-overwrite"
lOverwrite := .T.
ENDIF
i++
ENDDO
IF Empty( aTags )
aTags := { { "<?prg", "?>" } }
ENDIF
IF !Empty( cFileMask )
hb_FNameSplit( cFileMask, @cPath, NIL, NIL )
aFiles := Directory( cFileMask )
IF !Empty( aFiles )
FOR i := 1 TO Len( aFiles )
cFile := cPath + aFiles[i, 1]
? "Processing: " + cFile + " ..."
cResult := MixMinifyFile( cFile, aTags )
cOutFile := iif( lOverwrite, cFile, cFile + ".min" )
IF hb_MemoWrit( cOutFile, cResult )
? " -> [Success] " + iif( lOverwrite, "Overwritten", "Output to: " + cOutFile )
ELSE
? " -> [Error] Cannot write file"
ENDIF
NEXT
ENDIF
ENDIF
RETURN NIL
FUNCTION MixMinifyFile( cFileName, aTags )
LOCAL cSource
IF !File( cFileName )
RETURN ""
ENDIF
cSource := hb_MemoRead( cFileName )
RETURN MixMinifyString( cSource, aTags )
FUNCTION MixMinifyString( cText, aTags )
LOCAL cResult := "", nMinPos, nStartPos, nEndPos, nTagIndex, i
LOCAL cJSBlock, cKeepBlock, cStartTag, cEndTag
IF aTags == NIL .OR. Empty( aTags )
aTags := { { "<?prg", "?>" } }
ENDIF
DO WHILE .T.
nMinPos := 0; nTagIndex := 0
FOR i := 1 TO Len( aTags )
nStartPos := At( aTags[i, 1], cText )
IF nStartPos > 0 .AND. (nMinPos == 0 .OR. nStartPos < nMinPos)
nMinPos := nStartPos; nTagIndex := i
ENDIF
NEXT
IF nMinPos == 0
IF Len( cText ) > 0
cResult += JSMin( cText )
ENDIF
EXIT
ENDIF
cStartTag := aTags[ nTagIndex, 1 ]; cEndTag := aTags[ nTagIndex, 2 ]
cJSBlock := SubStr( cText, 1, nMinPos - 1 )
IF Len( cJSBlock ) > 0
cResult += JSMin( cJSBlock )
ENDIF
cText := SubStr( cText, nMinPos )
nEndPos := At( cEndTag, cText )
IF nEndPos == 0
cResult += cText
EXIT
ENDIF
cKeepBlock := SubStr( cText, 1, nEndPos + Len( cEndTag ) - 1 )
cResult += cKeepBlock
cText := SubStr( cText, nEndPos + Len( cEndTag ) )
ENDDO
RETURN cResult
#pragma BEGINDUMP
#include "hbapi.h"
#include "hbapistr.h"
#include <stdlib.h>
#include <string.h>
typedef struct {
const char * in_buf; int in_len; int in_pos;
char * out_buf; int out_len; int out_pos;
int theA; int theB; int theLookahead; int theX; int theY;
} JSMIN_STATE;
static int jsmin_getc( JSMIN_STATE * s ) {
if ( s->in_pos < s->in_len ) return (unsigned char)s->in_buf[s->in_pos++];
return EOF;
}
static void jsmin_putc( int c, JSMIN_STATE * s ) {
if ( s->out_pos >= s->out_len - 1 ) {
s->out_len *= 2;
s->out_buf = (char *)hb_xrealloc( s->out_buf, s->out_len );
}
s->out_buf[s->out_pos++] = (char)c;
}
static int isAlphanum( int c ) {
return ( ( c >= 'a' && c <= 'z' ) || ( c >= '0' && c <= '9' ) ||
( c >= 'A' && c <= 'Z' ) || c == '_' || c == '$' || c == '\\' || c > 126 );
}
static int jsmin_get( JSMIN_STATE * s ) {
int c = s->theLookahead;
s->theLookahead = EOF;
if ( c == EOF ) c = jsmin_getc( s );
if ( c >= ' ' || c == '\n' || c == EOF ) return c;
if ( c == '\r' ) return '\n';
return ' ';
}
static int jsmin_peek( JSMIN_STATE * s ) {
s->theLookahead = jsmin_get( s );
return s->theLookahead;
}
static int jsmin_next( JSMIN_STATE * s ) {
int c = jsmin_get( s );
if ( c == '/' ) {
switch ( jsmin_peek( s ) ) {
case '/': for (;;) { c = jsmin_get( s ); if ( c <= '\n' ) break; } break;
case '*': jsmin_get( s );
for (;;) {
switch ( jsmin_get( s ) ) {
case '*': if ( jsmin_peek( s ) == '/' ) { jsmin_get( s ); c = ' '; goto done_comment; } break;
case EOF: return EOF;
}
}
done_comment: break;
}
}
s->theY = s->theX; s->theX = c; return c;
}
static void jsmin_action( JSMIN_STATE * s, int d ) {
switch ( d ) {
case 1:
jsmin_putc( s->theA, s );
if ( (s->theY == '\n' || s->theY == ' ') &&
(s->theA == '+' || s->theA == '-' || s->theA == '*' || s->theA == '/') &&
(s->theB == '+' || s->theB == '-' || s->theB == '*' || s->theB == '/') ) {
jsmin_putc( s->theY, s );
}
case 2:
s->theA = s->theB;
if ( s->theA == '\'' || s->theA == '"' || s->theA == '`' ) {
for (;;) {
jsmin_putc( s->theA, s ); s->theA = jsmin_get( s );
if ( s->theA == s->theB ) break;
if ( s->theA == '\\' ) { jsmin_putc( s->theA, s ); s->theA = jsmin_get( s ); }
if ( s->theA == EOF ) return;
}
}
case 3:
s->theB = jsmin_next( s );
if ( s->theB == '/' && ( s->theA == '(' || s->theA == ',' || s->theA == '=' || s->theA == ':' ||
s->theA == '[' || s->theA == '!' || s->theA == '&' || s->theA == '|' || s->theA == '?' ||
s->theA == '+' || s->theA == '-' || s->theA == '~' || s->theA == '*' || s->theA == '/' ||
s->theA == '{' || s->theA == '\n' ) ) {
jsmin_putc( s->theA, s );
if ( s->theA == '/' || s->theA == '*' ) jsmin_putc( ' ', s );
jsmin_putc( s->theB, s );
for (;;) {
s->theA = jsmin_get( s );
if ( s->theA == '[' ) {
for (;;) {
jsmin_putc( s->theA, s ); s->theA = jsmin_get( s );
if ( s->theA == ']' ) break;
if ( s->theA == '\\' ) { jsmin_putc( s->theA, s ); s->theA = jsmin_get( s ); }
if ( s->theA == EOF ) return;
}
}
if ( s->theA == '/' ) {
switch ( jsmin_peek( s ) ) { case '/': case '*': return; } break;
}
if ( s->theA == '\\' ) { jsmin_putc( s->theA, s ); s->theA = jsmin_get( s ); }
if ( s->theA == EOF ) return;
jsmin_putc( s->theA, s );
}
s->theB = jsmin_next( s );
}
}
}
static void jsmin_process( JSMIN_STATE * s ) {
s->theLookahead = EOF; s->theX = EOF; s->theY = EOF; s->theA = '\n';
jsmin_action( s, 3 );
while ( s->theA != EOF ) {
switch ( s->theA ) {
case ' ':
if ( isAlphanum( s->theB ) ) jsmin_action( s, 1 ); else jsmin_action( s, 2 ); break;
case '\n':
switch ( s->theB ) {
case '{': case '[': case '(': case '+': case '-': case '!': case '~': jsmin_action( s, 1 ); break;
case ' ': jsmin_action( s, 3 ); break;
default: if ( isAlphanum( s->theB ) ) jsmin_action( s, 1 ); else jsmin_action( s, 2 );
} break;
default:
switch ( s->theB ) {
case ' ': if ( isAlphanum( s->theA ) ) { jsmin_action( s, 1 ); break; } jsmin_action( s, 3 ); break;
case '\n':
switch ( s->theA ) {
case '}': case ']': case ')': case '+': case '-': case '"': case '\'': case '`': jsmin_action( s, 1 ); break;
default: if ( isAlphanum( s->theA ) ) jsmin_action( s, 1 ); else jsmin_action( s, 3 );
} break;
default: jsmin_action( s, 1 ); break;
}
}
}
}
HB_FUNC( JSMIN ) {
const char * szInput = hb_parc( 1 );
int nLen = (int)hb_parclen( 1 );
JSMIN_STATE state;
if ( !szInput || nLen == 0 ) { hb_retc( "" ); return; }
memset( &state, 0, sizeof( JSMIN_STATE ) );
state.in_buf = szInput; state.in_len = nLen; state.in_pos = 0;
state.out_len = nLen + 1; state.out_buf = (char *)hb_xalloc( state.out_len ); state.out_pos = 0;
jsmin_process( &state );
state.out_buf[state.out_pos] = '\0';
hb_retclen_buffer( state.out_buf, state.out_pos );
}
#pragma ENDDUMPThis tool has saved me from countless manual copy-paste operations. I hope it helps other developers using mod_harbour! Feedback and testing are welcome.
WeChat ID: ssbbstw