feat(webui): turn the Web UI into a single-page application

index.html now contains all pages as sections (overview, ports, stat,
vlan, l2, mirror, lag, eee, bandwidth, system, update); the sidebar
switches between them via the URL hash (#/ports, #/vlan, ...).
login.html stays separate as the authentication gate.

A section's initialisation runs when the section is shown, its polling
intervals run only while it is visible, and re-entering a section
refetches the data; the checkbox builders (lag/vlan/mirror) and the L2
scanner were made idempotent so re-initialising cannot duplicate rows.

tools/minify.py: recognise regex literals - an escaped slash before the
closing delimiter was previously stripped as a // comment.

Measured (make MACHINE=KP_9000_6XHML_X2): 8 embedded files, 109,814 ->
97,730 -> 25,302 bytes gzip (23.0 %); the data block ends at 0x462d6
(~86 KB freed).  On a real PCB-K0402WS-V3: cold load ~450 ms, section
switches ~60-160 ms with no asset re-downloads, all sections render
without JavaScript errors.
This commit is contained in:
eraiza0816 committed 2026-08-11 06:37:20 +09:00
1 parent b651890a3a
commit 67ae4598d9
13 files changed
+384 -425

No files matched your search

-18
View File
@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="bw_title">Ingress and Egress Bandwidth</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1 data-i18n="bw_heading">Ingress and Egress Bandwidth</h1>
<table id="bwtable">
<tr> <th> </th> <th colspan="3" data-i18n="bw_ingress"> Ingress </th> <th colspan="2" data-i18n="bw_egress">Egress</th> <th></th></tr>
<tr> <th data-i18n="bw_col_port">Port</th> <th data-i18n="bw_col_limit">Limit</th> <th data-i18n="bw_col_bandwidth">Bandwidth [kBit/s]</th> <th data-i18n="bw_col_flow">Flow Control</th> <th data-i18n="bw_col_limit">Limit</th> <th data-i18n="bw_col_bandwidth">Bandwidth [kBit/s]</th> <th data-i18n="bw_col_apply">Apply</th></tr>
</table>
</div>
</body>
</html>
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="eee_title">EEE Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1 data-i18n="eee_heading">EEE Status</h1>
<table id="eeetable">
<tr> <th> </th> <th colspan="3" data-i18n="eee_advertising"> Advertising </th> <th colspan="3" data-i18n="eee_partner">Link-Partner advertises</th> <th></th></tr>
<tr> <th data-i18n="eee_port">Port</th> <th>2.5G</th> <th>1G</th> <th>100M</th> <th>2.5G</th> <th>1G</th> <th>100M</th> <th data-i18n="eee_active">Active?</th></tr>
</table>
<div>
<input style="width:20%;" class="action" id="eee_enable" onclick="eeeSub(0, 1);" type="button" data-i18n="eee_enable" value="Enable EEE">
<input style="width:20%;" class="action" id="eee_disable" onclick="eeeSub(0, 0);" type="button" data-i18n="eee_disable" value="Disable EEE">
</div>
</div>
</body>
</html>
+255 -10
View File
@@ -1,21 +1,41 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<script src="/main.js"></script> <script src="/main.js"></script>
<script>
window.addEventListener("load", function() {
update( () => {
const interval = setInterval(update, 2000);
});
});
</script>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<title data-i18n="index_title">FreeSwitchOS Main Page</title> <title data-i18n="index_title">FreeSwitchOS Main Page</title>
<style>
.popup {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.popup-content {
background: #fff;
padding: 20px;
border-radius: 5px;
text-align: center;
height: 80%
}
.popup .popup-content {
overflow-y: scroll;
}
.tab-bar { display: flex; border-bottom: 2px solid #226; margin-bottom: 0; padding: 1px 16px; padding-bottom: 0; }
.tab-btn { padding: 10px 20px; background-color: #ddf; border: none; cursor: pointer; font-size: 1em; border-radius: 8px 8px 0 0; margin-right: 4px; }
.tab-btn:hover { background-color: #aaf; }
.tab-btn.active { background-color: #aaf; font-weight: bold; border-bottom: 2px solid #aaf; margin-bottom: -2px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
</style>
</head> </head>
<body> <body>
<nav id="sidebar"></nav> <nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;"> <div id="ports" style="margin-left:16%;padding:1px 16px"></div>
<div id="ports"></div>
<div class="page" id="page-overview" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="index_heading">Switch Configuration</h1> <h1 data-i18n="index_heading">Switch Configuration</h1>
<table id="infoTable"> <table id="infoTable">
<tr> <tr>
@@ -25,5 +45,230 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="page" id="page-ports" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="port_heading">Port Configuration</h1>
<table id="speedtable">
<tr> <th data-i18n="port_col_port">Port</th> <th data-i18n="port_col_name">Name</th> <th data-i18n="port_col_speed">Current Link Speed</th><th data-i18n="port_col_set_speed">Set Speed</th><th data-i18n="port_col_disabled">Disabled</th><th data-i18n="port_col_apply">Apply</th></tr>
</table>
<h2 style="margin-top:3em" data-i18n="port_mtu_heading">Configure Maximum Frame Size (MTU) forwarded at Port</h2>
<table id="mtutable" style="margin-top:1em">
</table>
</div>
<div class="page" id="page-stat" style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="popup" class="popup">
<div class="popup-content">
<h2 data-i18n="stat_detailed">Detailed Port Statistics</h2>
<div id="popup_text"></div>
<button id="closePopup" class="action" data-i18n="stat_close">Close</button>
</div>
</div>
<h1 data-i18n="stat_heading">Port Statistics</h1>
<table id="statstable">
<tr> <th data-i18n="stat_col_port">Port</th> <th data-i18n="stat_col_name">Name</th> <th data-i18n="stat_col_link">link</th> <th data-i18n="stat_col_tx_good">TX Good</th> <th data-i18n="stat_col_tx_bad">TX Bad</th> <th data-i18n="stat_col_rx_good">RX Good</th> <th data-i18n="stat_col_rx_bad">RX Bad</th> <th data-i18n="stat_col_all"> All Counters </th></tr>
</table>
</div>
<div class="page" id="page-vlan" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="vlan_heading">VLAN Configuration</h1>
<form id="vform">
<div>
<label for="vlanSelect" data-i18n="vlan_select">VLAN Select:</label>
<select id="vlanSelect" style="margin: 0 0 0 8px">
<option value="" disabled selected data-i18n="vlan_choose">— VLAN Choose —</option>
</select>
</div>
<br/>
<div>
<label for="vid" data-i18n="vlan_id">VLAN ID:</label>
<input type="number" min="1" max="4094" id="vid" name="vid">
<button type="button" style="margin: 0 0 0 24px" onclick="fetchVLAN();" data-i18n="vlan_get_config">Get Configuration</button>
</div>
<br/><br/>
<label for="vname" data-i18n="vlan_name">VLAN Name:</label>
<input type="text" id="vname" name="vname"><br><br>
<br/>
<h2 data-i18n="vlan_tagged">Tagged Ports</h2>
<div id="tPorts"><button type="button" style="transform: translateY(-100%);margin: 0 50px 0 0" onclick="utClicked(true);" data-i18n="vlan_select_all">Select all</button></div>
<h2 data-i18n="vlan_untagged">Untagged Ports</h2>
<div id="uPorts"><button type="button" style="transform: translateY(-100%); margin: 0 50px 0 0" onclick="utClicked(false);" data-i18n="vlan_select_all">Select all</button> </div>
<h2 data-i18n="vlan_pvid">Use as default VLAN for incoming traffic (PVID)</h2>
<div id="pPorts"><button type="button" style="transform: translateY(-100%); margin: 0 50px 0 0" onclick="pvClicked(true);" data-i18n="vlan_select_all">Select all</button> </div>
<br/> <input style="width:40%;" class="action" id="vlan_sub" onclick="vlanSub();" type="button" data-i18n="vlan_update" value="Update / Create">
</form>
<h2 data-i18n="vlan_configured">Configured VLANs</h2>
<table id="vlanTable" style="width:90%">
<thead>
<tr>
<th>VLAN</th>
<th data-i18n="vlan_col_name">Name</th>
<th data-i18n="vlan_col_member">Member Ports</th>
<th data-i18n="vlan_col_tagged">Tagged Ports</th>
<th data-i18n="vlan_col_untagged">Untagged Ports</th>
<th data-i18n="vlan_col_pvid">PVID Ports</th>
<th data-i18n="vlan_col_delete">Delete</th>
</tr>
</thead>
<tbody id="vlanTableBody">
</tbody>
</table>
</div>
<div class="page" id="page-l2" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="l2_heading">L2 Configuration</h1>
<p><span data-i18n="l2_shown">Shown:</span> <span id="l2count">-</span></p>
<table id="l2table">
<tr>
<th><span class="l2sort" onclick="l2SortBy('port')"><span data-i18n="l2_col_port">Port</span><span id="l2a_port" class="l2arrow"></span></span><br>
<input id="l2f_port" class="l2filter" oninput="l2FilterChanged()" size="5"></th>
<th><span class="l2sort" onclick="l2SortBy('mac')">MAC<span id="l2a_mac" class="l2arrow"></span></span><br>
<input id="l2f_mac" class="l2filter" oninput="l2FilterChanged()" size="14"></th>
<th><span class="l2sort" onclick="l2SortBy('vlan')">VLAN<span id="l2a_vlan" class="l2arrow"></span></span><br>
<input id="l2f_vlan" class="l2filter" oninput="l2FilterChanged()" size="5"></th>
<th><span class="l2sort" onclick="l2SortBy('type')"><span data-i18n="l2_col_type">Type</span><span id="l2a_type" class="l2arrow"></span></span><br>
<input id="l2f_type" class="l2filter" oninput="l2FilterChanged()" size="8"></th>
<th data-i18n="l2_col_remove">Remove Entry</th></tr>
</table>
</div>
<div class="page" id="page-mirror" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="mirror_heading">Mirror Configuration</h1>
<label class="tswitch"><span data-i18n="mirror_enabled">Enabled:</span> <input id="me" type="checkbox"></label><br/>
<label for="mp"><span data-i18n="mirror_port">Mirroring Port:</span></label> <input type="number" id="mp" name="mp" min="1" max="9"/>
<h2 data-i18n="mirror_tx">Mirrored Ports (TX)</h2>
<div id="mPortsTX"></div>
<br />
<h2 data-i18n="mirror_rx">Mirrored Ports (RX)</h2>
<div id="mPortsRX"></div>
<br/> <input style="width:15%;" class="action" id="mirror_sub" onclick="mirrorSub();" type="button" data-i18n="mirror_update" value="Update / Create">
<input style="width:15%;" class="action" id="mirror_del" onclick="mirrorDel();" type="button" data-i18n="mirror_disable" value="Disable Mirroring">
</div>
<div class="page" id="page-lag" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="lag_heading">Link Aggregation Groups Configuration</h1>
<h2>LAG 1 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub0" onclick="lagSub(0);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG0"></div>
<br />
<h2>LAG 2 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub1" onclick="lagSub(1);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG1"></div>
<br />
<h2>LAG 3 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub2" onclick="lagSub(2);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG2"></div>
<br />
<h2>LAG 4 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub3" onclick="lagSub(3);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG3"></div>
</div>
<div class="page" id="page-eee" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="eee_heading">EEE Status</h1>
<table id="eeetable">
<tr> <th> </th> <th colspan="3" data-i18n="eee_advertising"> Advertising </th> <th colspan="3" data-i18n="eee_partner">Link-Partner advertises</th> <th></th></tr>
<tr> <th data-i18n="eee_port">Port</th> <th>2.5G</th> <th>1G</th> <th>100M</th> <th>2.5G</th> <th>1G</th> <th>100M</th> <th data-i18n="eee_active">Active?</th></tr>
</table>
<div>
<input style="width:20%;" class="action" id="eee_enable" onclick="eeeSub(0, 1);" type="button" data-i18n="eee_enable" value="Enable EEE">
<input style="width:20%;" class="action" id="eee_disable" onclick="eeeSub(0, 0);" type="button" data-i18n="eee_disable" value="Disable EEE">
</div>
</div>
<div class="page" id="page-bandwidth" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="bw_heading">Ingress and Egress Bandwidth</h1>
<table id="bwtable">
<tr> <th> </th> <th colspan="3" data-i18n="bw_ingress"> Ingress </th> <th colspan="2" data-i18n="bw_egress">Egress</th> <th></th></tr>
<tr> <th data-i18n="bw_col_port">Port</th> <th data-i18n="bw_col_limit">Limit</th> <th data-i18n="bw_col_bandwidth">Bandwidth [kBit/s]</th> <th data-i18n="bw_col_flow">Flow Control</th> <th data-i18n="bw_col_limit">Limit</th> <th data-i18n="bw_col_bandwidth">Bandwidth [kBit/s]</th> <th data-i18n="bw_col_apply">Apply</th></tr>
</table>
</div>
<div class="page" id="page-system" style="margin-left:16%;padding:1px 16px;height:1000px;">
<div class="tab-bar">
<button class="tab-btn active" onclick="openTab(event, 'system-tab')" data-i18n="sys_tab_system">System</button>
<button class="tab-btn" onclick="openTab(event, 'advanced-tab')" data-i18n="sys_tab_advanced">Advanced</button>
<button class="tab-btn" onclick="openTab(event, 'console-tab')" data-i18n="sys_tab_console">Console</button>
</div>
<div id="system-tab" class="tab-content active">
<h1 data-i18n="sys_heading">System Settings</h1>
<div class="row">
<div class="lcol"> <label for="hostname" data-i18n="sys_hostname">Hostname:</label></div>
<div class="rcol"> <input id="hostname" type="text" maxlength="23" size="20"/>
</div>
</div>
<div class="row">
<div class="lcol"> <label for="ip" data-i18n="sys_ip">IP address:</label></div>
<div class="rcol"> <input id="ip" type="text" size="15"/>
</div>
</div>
<div class="row">
<div class="lcol"> <label for="netmask" data-i18n="sys_netmask">Netmask:</label></div>
<div class="rcol"> <input id="netmask" type="text" size="15"/>
</div>
</div>
<div class="row">
<div class="lcol"> <label for="gw" data-i18n="sys_gateway">Gateway:</label></div>
<div class="rcol"> <input id="gw" type="text" size="15"/>
</div>
</div>
<div class="row">
<div class="lcol"> <label for="model" data-i18n="sys_model">Model:</label></div>
<div class="rcol"> <span id="model">-</span>
</div>
</div>
<div class="row">
<div class="lcol"> <label for="lang-select" data-i18n="sys_language">Language:</label></div>
<div class="rcol">
<select id="lang-select">
<option value="en">English</option>
<option value="ja">日本語</option>
<option value="zh">中文</option>
</select>
</div>
</div>
<div class="row">
<div class="lcol"> <label for="mgmtvlan" data-i18n="sys_mgmt_vlan">Management VLAN:</label></div>
<div class="rcol">
<select id="mgmtvlan">
</select>
</div>
</div>
<br/>
<span data-i18n="sys_ip_note">When updating the above settings, remember to point your browser to the new IP afterwards:</span><br/>
<input style="width:40%;" class="action" id="ip_sub" onclick="ipSub();" type="button" data-i18n="sys_update" value="Update Settings"><br/>
<br/>
<span data-i18n="sys_save_label">Save all current settings to Flash:</span><br/>
<input style="width:40%;" class="action" id="flash_sub" onclick="flashSave();" type="button" data-i18n="sys_save" value="Save Settings to Flash">
</div>
<div id="advanced-tab" class="tab-content">
<h1 data-i18n="sys_advanced">Advanced Settings</h1>
<div class="lcol"> <label for="config_display" data-i18n="sys_startup_config">Startup configuration:</label></div>
<textarea id="config_display" rows="8" cols="60"></textarea>
<br/><br/>
<span data-i18n="sys_startup_warn">Be careful when saving the directly edited startup configuration, you can lock yourself out:</span><br/>
<input style="width:40%;" class="action" id="clear_config" onclick="clearConfig();" type="button" data-i18n="sys_clear_config" value="Clear Startup Config">
<br/>
<input style="width:40%;" class="action" id="flash_startup_sub" onclick="flashStartupSave();" type="button" data-i18n="sys_save_startup" value="Save Startup Settings to Flash">
<br/>
<input style="width:40%;" class="action" id="switch_reset" onclick="resetSwitch();" type="button" data-i18n="sys_reset" value="Reset Switch">
</div>
<div id="console-tab" class="tab-content">
<h1 data-i18n="sys_console">Console Command</h1>
<label for="console_command" data-i18n="sys_enter_cmd">Enter command:</label>
<input type="text" id="console_cmd" name="console_cmd" style="width:40%;">
<input style="width:20%;" class="action" id="cmd_sub" onclick="cmdSub();" type="button" data-i18n="sys_send_cmd" value="Send Command"><br/>
<br/><br/>
<span data-i18n="sys_console_warn">Be careful when entering console commands, you can lock yourself out!</span><br/>
</div>
</div>
<div class="page" id="page-update" style="margin-left:16%;padding:1px 16px;height:1000px;width:40%;">
<h1 data-i18n="update_heading">Firmware Update</h1>
<form enctype="multipart/form-data" action="/upload" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="1000000" />
<span data-i18n="update_instruction">Choose a firmware update file to upload:</span> <br/> <br/>
<input name="uploadedfile" type="file" accept=".bin" /><br />
<input style="margin-top:3em" type="submit" data-i18n="update_upload" value="Upload File" />
</form>
</div>
</body> </body>
</html> </html>
-27
View File
@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="l2_title">FreeSwitchOS L2 Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1 data-i18n="l2_heading">L2 Configuration</h1>
<p><span data-i18n="l2_shown">Shown:</span> <span id="l2count">-</span></p>
<table id="l2table">
<tr>
<th><span class="l2sort" onclick="l2SortBy('port')"><span data-i18n="l2_col_port">Port</span><span id="l2a_port" class="l2arrow"></span></span><br>
<input id="l2f_port" class="l2filter" oninput="l2FilterChanged()" size="5"></th>
<th><span class="l2sort" onclick="l2SortBy('mac')">MAC<span id="l2a_mac" class="l2arrow"></span></span><br>
<input id="l2f_mac" class="l2filter" oninput="l2FilterChanged()" size="14"></th>
<th><span class="l2sort" onclick="l2SortBy('vlan')">VLAN<span id="l2a_vlan" class="l2arrow"></span></span><br>
<input id="l2f_vlan" class="l2filter" oninput="l2FilterChanged()" size="5"></th>
<th><span class="l2sort" onclick="l2SortBy('type')"><span data-i18n="l2_col_type">Type</span><span id="l2a_type" class="l2arrow"></span></span><br>
<input id="l2f_type" class="l2filter" oninput="l2FilterChanged()" size="8"></th>
<th data-i18n="l2_col_remove">Remove Entry</th></tr>
</table>
</div>
</body>
</html>
-25
View File
@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="lag_title">Link Aggregation Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1 data-i18n="lag_heading">Link Aggregation Groups Configuration</h1>
<h2>LAG 1 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub0" onclick="lagSub(0);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG0"></div>
<br />
<h2>LAG 2 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub1" onclick="lagSub(1);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG1"></div>
<br />
<h2>LAG 3 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub2" onclick="lagSub(2);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG2"></div>
<br />
<h2>LAG 4 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub3" onclick="lagSub(3);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG3"></div>
</div>
</body>
</html>
+97 -51
View File
@@ -899,17 +899,17 @@ document.addEventListener('DOMContentLoaded', function() {
var sidebarEl = document.getElementById('sidebar'); var sidebarEl = document.getElementById('sidebar');
if (sidebarEl) if (sidebarEl)
sidebarEl.innerHTML = sidebarEl.innerHTML =
"<ul><li><a href='index.html' data-i18n='nav_overview'>Overview</a></li>" "<ul><li><a href='#/overview' data-i18n='nav_overview'>Overview</a></li>"
+ "<li><a href='ports.html' data-i18n='nav_port_config'>Port Configuration</a></li>" + "<li><a href='#/ports' data-i18n='nav_port_config'>Port Configuration</a></li>"
+ "<li><a href='stat.html' data-i18n='nav_port_stat'>Port Statistics</a></li>" + "<li><a href='#/stat' data-i18n='nav_port_stat'>Port Statistics</a></li>"
+ "<li><a href='vlan.html' >VLAN</a></li>" + "<li><a href='#/vlan' >VLAN</a></li>"
+ "<li><a href='l2.html' data-i18n='nav_l2'>L2 Configuration</a></li>" + "<li><a href='#/l2' data-i18n='nav_l2'>L2 Configuration</a></li>"
+ "<li><a href='mirror.html' data-i18n='nav_mirror'>Mirroring</a></li>" + "<li><a href='#/mirror' data-i18n='nav_mirror'>Mirroring</a></li>"
+ "<li><a href='lag.html' data-i18n='nav_lag'>Link Aggregation</a></li>" + "<li><a href='#/lag' data-i18n='nav_lag'>Link Aggregation</a></li>"
+ "<li><a href='eee.html' data-i18n='nav_eee'>EEE</a></li>" + "<li><a href='#/eee' data-i18n='nav_eee'>EEE</a></li>"
+ "<li><a href='bandwidth.html' data-i18n='nav_bandwidth'>Bandwidth Limits</a></li>" + "<li><a href='#/bandwidth' data-i18n='nav_bandwidth'>Bandwidth Limits</a></li>"
+ "<li><a href='system.html' data-i18n='nav_system'>System Settings</a></li>" + "<li><a href='#/system' data-i18n='nav_system'>System Settings</a></li>"
+ "<li><a href='update.html' data-i18n='nav_fw_update'>Firmware Update</a></li></ul>"; + "<li><a href='#/update' data-i18n='nav_fw_update'>Firmware Update</a></li></ul>";
}); });
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
@@ -920,6 +920,54 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
/* Single-page navigation: the sidebar links set the URL hash
* ("#/ports" and friends) and the matching section in index.html is
* shown. Each section initialises on first display, and its polling
* intervals run only while the section is visible. */
var sectionIntervals = [];
var sectionInits = {};
function setSectionInterval(fn, ms) {
sectionIntervals.push(setInterval(fn, ms));
}
function clearSectionIntervals() {
sectionIntervals.forEach(function(t) { clearInterval(t); });
sectionIntervals = [];
}
function showSection(name) {
document.querySelectorAll('.page').forEach(function(el) { el.style.display = 'none'; });
var sec = document.getElementById('page-' + name);
if (sec) sec.style.display = 'block';
clearSectionIntervals();
if (sectionInits[name])
sectionInits[name]();
}
window.addEventListener('hashchange', function() {
showSection((location.hash || '#/overview').replace(/^#\//, ''));
});
document.addEventListener('DOMContentLoaded', function() {
showSection((location.hash || '#/overview').replace(/^#\//, ''));
});
sectionInits.overview = function() {
update( () => {
setSectionInterval(update, 2000);
});
};
/* System page tabs (system.html section of index.html) */
function openTab(evt, tabId) {
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
evt.currentTarget.classList.add('active');
}
var configInterval = Number(); var configInterval = Number();
var configuration = []; var configuration = [];
const conf_cmds = [ const conf_cmds = [
@@ -1195,12 +1243,11 @@ function resetSwitch() {
}, 3000); }, 3000);
} }
window.addEventListener("load", function() { sectionInits.system = function() {
if (!document.getElementById('ip')) return;
var langSel = document.getElementById('lang-select'); var langSel = document.getElementById('lang-select');
if (langSel) langSel.value = rtlLang; if (langSel) langSel.value = rtlLang;
systemInterval = setInterval(fetchIP, 1000); systemInterval = setSectionInterval(fetchIP, 1000);
}); };
var mgmtVlanCurrent = 0; var mgmtVlanCurrent = 0;
@@ -1382,16 +1429,15 @@ function getMTUs() {
xhttp.timeout = 1500; sendXHTTP(xhttp); xhttp.timeout = 1500; sendXHTTP(xhttp);
} }
window.addEventListener("load", function() { sectionInits.ports = function() {
if (!document.getElementById('speedtable')) return;
update( () => { update( () => {
createPortTable(); createPortTable();
updatePortTable(); updatePortTable();
getMTUs() getMTUs()
const interval = setInterval(update, 2000); setSectionInterval(update, 2000);
const updatePortTableInterval = setInterval(updatePortTable, 1000); setSectionInterval(updatePortTable, 1000);
});
}); });
};
const mib_counters = [ const mib_counters = [
"Interface in Octets", 8, "Interface in Octets", 8,
@@ -1595,15 +1641,14 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
window.addEventListener("load", function() { sectionInits.stat = function() {
if (!document.getElementById('statstable')) return;
update( () => { update( () => {
update(); update();
fillStats(); fillStats();
const stat = setInterval(fillStats, 1000); setSectionInterval(fillStats, 1000);
const interval = setInterval(update, 2000); setSectionInterval(update, 2000);
});
}); });
};
var l2GetInterval; var l2GetInterval;
var l2Entries = []; var l2Entries = [];
@@ -1765,20 +1810,23 @@ function getL2() {
xhttp.timeout = 1500; sendXHTTP(xhttp); xhttp.timeout = 1500; sendXHTTP(xhttp);
} }
window.addEventListener("load", function() { sectionInits.l2 = function() {
if (!document.getElementById('l2table')) return; l2Entries = [];
l2CurrentEntry = 0;
update( () => { update( () => {
getL2(); getL2();
const interval = setInterval(update, 2000); setSectionInterval(update, 2000);
l2GetInterval = setInterval(getL2, 1000); l2GetInterval = setSectionInterval(getL2, 1000);
});;
}); });
};
var vlanInterval = Number(); var vlanInterval = Number();
function vlanForm() { function vlanForm() {
if (!numPorts) if (!numPorts)
return; return;
document.querySelectorAll('#tPorts .cbgroup, #uPorts .cbgroup, #pPorts .cbgroup')
.forEach(function(el) { el.remove(); });
var t = document.getElementById('tPorts'); var t = document.getElementById('tPorts');
var u = document.getElementById('uPorts'); var u = document.getElementById('uPorts');
var p = document.getElementById('pPorts'); var p = document.getElementById('pPorts');
@@ -1974,8 +2022,7 @@ function loadVlanList() {
sendXHTTP(xhttp); sendXHTTP(xhttp);
} }
window.addEventListener("load", function() { sectionInits.vlan = function() {
if (!document.getElementById('vlanSelect')) return;
update( () => { update( () => {
vlanForm(); vlanForm();
refreshVlanViews(); refreshVlanViews();
@@ -1983,9 +2030,9 @@ window.addEventListener("load", function() {
document.getElementById('vid').value = this.value; document.getElementById('vid').value = this.value;
fetchVLAN(); fetchVLAN();
}; };
const interval = setInterval(update, 2000); setSectionInterval(update, 2000);
});
}); });
};
async function vlanSub() { async function vlanSub() {
var commands = []; var commands = [];
@@ -2028,6 +2075,8 @@ var lagInterval = Number();
function lagForm() { function lagForm() {
if (!numPorts) if (!numPorts)
return; return;
for (let j = 0; j < 4; j++)
document.getElementById("mLAG" + j).innerHTML = '';
for (let j=0; j < 4; j++) { for (let j=0; j < 4; j++) {
var lag = "mLAG" + j var lag = "mLAG" + j
console.log("Adding LAG " + lag) console.log("Adding LAG " + lag)
@@ -2098,13 +2147,12 @@ async function lagSub(l) {
} }
} }
window.addEventListener("load", function() { sectionInits.lag = function() {
if (!document.getElementById('mLAG0')) return;
update( () => { update( () => {
lagForm(); lagForm();
const interval = setInterval(update, 2000); setSectionInterval(update, 2000);
});
}); });
};
var mirrorInterval = Number(); var mirrorInterval = Number();
const mirrors = ["mPortsTX", "mPortsRX"]; const mirrors = ["mPortsTX", "mPortsRX"];
@@ -2112,6 +2160,7 @@ const mirrors = ["mPortsTX", "mPortsRX"];
function mirrorForm() { function mirrorForm() {
if (!numPorts) if (!numPorts)
return; return;
mirrors.forEach(function(m) { document.getElementById(m).innerHTML = ''; });
for (let j=0; j < mirrors.length; j++) { for (let j=0; j < mirrors.length; j++) {
console.log("Adding Mirror " + j) console.log("Adding Mirror " + j)
var m = document.getElementById(mirrors[j]); var m = document.getElementById(mirrors[j]);
@@ -2164,13 +2213,12 @@ function fetchMirror() {
sendXHTTP(xhttp); sendXHTTP(xhttp);
} }
window.addEventListener("load", function() { sectionInits.mirror = function() {
if (!document.getElementById('mPortsTX')) return;
update( () => { update( () => {
mirrorForm(); mirrorForm();
const interval = setInterval(update, 2000); setSectionInterval(update, 2000);
});
}); });
};
async function mirrorSub() { async function mirrorSub() {
var cmd = "mirror "; var cmd = "mirror ";
@@ -2260,15 +2308,14 @@ function getEEE() {
xhttp.timeout = 1500; sendXHTTP(xhttp); xhttp.timeout = 1500; sendXHTTP(xhttp);
} }
window.addEventListener("load", function() { sectionInits.eee = function() {
if (!document.getElementById('eeetable')) return;
update( () => { update( () => {
createEEE(); createEEE();
getEEE(); getEEE();
const interval = setInterval(update, 2000); setSectionInterval(update, 2000);
const iCount = setInterval(getEEE, 2000); setSectionInterval(getEEE, 2000);
});
}); });
};
async function eeeSub(port, enable) { async function eeeSub(port, enable) {
var cmd = "eee "; var cmd = "eee ";
@@ -2419,14 +2466,13 @@ function getBW() {
xhttp.timeout = 1500; sendXHTTP(xhttp); xhttp.timeout = 1500; sendXHTTP(xhttp);
} }
window.addEventListener("load", function() { sectionInits.bandwidth = function() {
if (!document.getElementById('bwtable')) return;
update( () => { update( () => {
createBW(); createBW();
getBW(); getBW();
const interval = setInterval(update, 2000); setSectionInterval(update, 2000);
});
}); });
};
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
if (!document.getElementById('infoTable')) return; if (!document.getElementById('infoTable')) return;
-23
View File
@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="mirror_title">Mirror Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1 data-i18n="mirror_heading">Mirror Configuration</h1>
<label class="tswitch"><span data-i18n="mirror_enabled">Enabled:</span> <input id="me" type="checkbox"></label><br/>
<label for="mp"><span data-i18n="mirror_port">Mirroring Port:</span></label> <input type="number" id="mp" name="mp" min="1" max="9"/>
<h2 data-i18n="mirror_tx">Mirrored Ports (TX)</h2>
<div id="mPortsTX"></div>
<br />
<h2 data-i18n="mirror_rx">Mirrored Ports (RX)</h2>
<div id="mPortsRX"></div>
<br/> <input style="width:15%;" class="action" id="mirror_sub" onclick="mirrorSub();" type="button" data-i18n="mirror_update" value="Update / Create">
<input style="width:15%;" class="action" id="mirror_del" onclick="mirrorDel();" type="button" data-i18n="mirror_disable" value="Disable Mirroring">
</div>
</body>
</html>
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="port_title">FreeSwitchOS Port Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1 data-i18n="port_heading">Port Configuration</h1>
<form id="vform" action="/vlan.html">
<table id="speedtable">
<tr> <th data-i18n="port_col_port">Port</th> <th data-i18n="port_col_name">Name</th> <th data-i18n="port_col_speed">Current Link Speed</th><th data-i18n="port_col_set_speed">Set Speed</th><th data-i18n="port_col_disabled">Disabled</th><th data-i18n="port_col_apply">Apply</th></tr>
</table>
<h2 style="margin-top:3em" data-i18n="port_mtu_heading">Configure Maximum Frame Size (MTU) forwarded at Port</h2>
<table id="mtutable" style="margin-top:1em">
</table>
</form>
</div>
</body>
</html>
-45
View File
@@ -1,45 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="stat_title">FreeSwitchOS Port Statistics</title>
<style>
.popup {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.popup-content {
background: #fff;
padding: 20px;
border-radius: 5px;
text-align: center;
height: 80%
}
.popup .popup-content {
overflow-y: scroll;
}
</style>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<div id="popup" class="popup">
<div class="popup-content">
<h2 data-i18n="stat_detailed">Detailed Port Statistics</h2>
<div id="popup_text"></div>
<button id="closePopup" class="action" data-i18n="stat_close">Close</button>
</div>
</div>
<h1 data-i18n="stat_heading">Port Statistics</h1>
<table id="statstable">
<tr> <th data-i18n="stat_col_port">Port</th> <th data-i18n="stat_col_name">Name</th> <th data-i18n="stat_col_link">link</th> <th data-i18n="stat_col_tx_good">TX Good</th> <th data-i18n="stat_col_tx_bad">TX Bad</th> <th data-i18n="stat_col_rx_good">RX Good</th> <th data-i18n="stat_col_rx_bad">RX Bad</th> <th data-i18n="stat_col_all"> All Counters </th></tr>
</table>
</div>
</body>
</html>
-104
View File
@@ -1,104 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script src="/main.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="sys_title">System Settings</title>
<style>
.tab-bar { display: flex; border-bottom: 2px solid #226; margin-bottom: 0; margin-left: 16%; padding: 1px 16px; padding-bottom: 0; }
.tab-btn { padding: 10px 20px; background-color: #ddf; border: none; cursor: pointer; font-size: 1em; border-radius: 8px 8px 0 0; margin-right: 4px; }
.tab-btn:hover { background-color: #aaf; }
.tab-btn.active { background-color: #aaf; font-weight: bold; border-bottom: 2px solid #aaf; margin-bottom: -2px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
</style>
</head>
<body>
<div class="tab-bar">
<button class="tab-btn active" onclick="openTab(event, 'system-tab')" data-i18n="sys_tab_system">System</button>
<button class="tab-btn" onclick="openTab(event, 'advanced-tab')" data-i18n="sys_tab_advanced">Advanced</button>
<button class="tab-btn" onclick="openTab(event, 'console-tab')" data-i18n="sys_tab_console">Console</button>
</div>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<div id="system-tab" class="tab-content active">
<h1 data-i18n="sys_heading">System Settings</h1>
<div class="row">
<div class="lcol"> <label for="hostname" data-i18n="sys_hostname">Hostname:</label></div>
<div class="rcol"> <input id="hostname" type="text" maxlength="23" size="20"/>
<button onclick="hostSub()" data-i18n="sys_apply">Apply</button></div>
</div>
<div class="row">
<div class="lcol"> <label data-i18n="sys_model">Model:</label></div>
<div class="rcol"><span id="model" class="rotext"></span></div>
</div>
<div class="row">
<div class="lcol"> <label for="ip" data-i18n="sys_ip">IP address:</label></div>
<div class="rcol"> <input id="ip" class="ip" type="text" minlength="7" maxlength="15" size="15"/></div>
</div>
<div class="row">
<div class="lcol"> <label for="netmask" data-i18n="sys_netmask">Netmask:</label></div>
<div class="rcol"><input id="netmask" class="ip" type="text" minlength="7" maxlength="15" size="15"/></div>
</div>
<div class="row">
<div class="lcol"> <label for="gw" data-i18n="sys_gateway">Gateway:</label></div>
<div class="rcol"><input id="gw" class="ip" type="text" minlength="7" maxlength="15" size="15"/></div>
</div>
<div class="row">
<div class="lcol"> <label for="mgmtvlan" data-i18n="sys_mgmt_vlan">Management VLAN:</label></div>
<div class="rcol"><select id="mgmtvlan" class="ip" onchange="mgmtVlanChanged()"></select></div>
</div>
<div class="row">
<div class="lcol"> <label data-i18n="sys_language">Language:</label></div>
<div class="rcol">
<select id="lang-select" onchange="changeLang()">
<option value="en">English</option>
<option value="ja">日本語</option>
<option value="zh">中文</option>
</select>
</div>
</div>
<br/>
<span data-i18n="sys_ip_note">When updating the above settings, remember to point your browser to the new IP afterwards:</span><br/>
<input style="width:40%;" class="action" id="ip_sub" onclick="ipSub();" type="button" data-i18n="sys_update" value="Update Settings"><br/>
<br/>
<span data-i18n="sys_save_label">Save all current settings to Flash:</span><br/>
<input style="width:40%;" class="action" id="flash_sub" onclick="flashSave();" type="button" data-i18n="sys_save" value="Save Settings to Flash">
</div>
<div id="advanced-tab" class="tab-content">
<h1 data-i18n="sys_advanced">Advanced Settings</h1>
<div class="lcol"> <label for="config_display" data-i18n="sys_startup_config">Startup configuration:</label></div>
<textarea id="config_display" rows="8" cols="60"></textarea>
<br/><br/>
<span data-i18n="sys_startup_warn">Be careful when saving the directly edited startup configuration, you can lock yourself out:</span><br/>
<input style="width:40%;" class="action" id="clear_config" onclick="clearConfig();" type="button" data-i18n="sys_clear_config" value="Clear Startup Config">
<br/>
<input style="width:40%;" class="action" id="flash_startup_sub" onclick="flashStartupSave();" type="button" data-i18n="sys_save_startup" value="Save Startup Settings to Flash">
<br/>
<input style="width:40%;" class="action" id="switch_reset" onclick="resetSwitch();" type="button" data-i18n="sys_reset" value="Reset Switch">
</div>
<div id="console-tab" class="tab-content">
<h1 data-i18n="sys_console">Console Command</h1>
<label for="console_command" data-i18n="sys_enter_cmd">Enter command:</label>
<input type="text" id="console_cmd" name="console_cmd" style="width:40%;">
<input style="width:20%;" class="action" id="cmd_sub" onclick="cmdSub();" type="button" data-i18n="sys_send_cmd" value="Send Command"><br/>
<br/><br/>
<span data-i18n="sys_console_warn">Be careful when entering console commands, you can lock yourself out!</span><br/>
</div>
</div>
<script>
function openTab(evt, tabId) {
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
evt.currentTarget.classList.add('active');
}
</script>
</body>
</html>
-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script src="/main.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="update_title">Firmware update</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;width:40%;">
<h1 data-i18n="update_heading">Firmware Update</h1>
<form enctype="multipart/form-data" action="/upload" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="1000000" />
<span data-i18n="update_instruction">Choose a firmware update file to upload:</span> <br/> <br/>
<input name="uploadedfile" type="file" accept=".bin" /><br />
<input style="margin-top:3em" type="submit" data-i18n="update_upload" value="Upload File" />
</form>
</body>
</html>
-55
View File
@@ -1,55 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="vlan_title">FreeSwitchOS VLAN Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1 data-i18n="vlan_heading">VLAN Configuration</h1>
<form id="vform" action="/vlan.html">
<div>
<label for="vlanSelect" data-i18n="vlan_select">VLAN Select:</label>
<select id="vlanSelect" style="margin: 0 0 0 8px">
<option value="" disabled selected data-i18n="vlan_choose">— VLAN Choose —</option>
</select>
</div>
<br/>
<div>
<label for="vid" data-i18n="vlan_id">VLAN ID:</label>
<input type="number" min="1" max="4094" id="vid" name="vid">
<button type="button" style="margin: 0 0 0 24px" onclick="fetchVLAN();" data-i18n="vlan_get_config">Get Configuration</button>
</div>
<br/><br/>
<label for="vname" data-i18n="vlan_name">VLAN Name:</label>
<input type="text" id="vname" name="vname"><br><br>
<br/>
<h2 data-i18n="vlan_tagged">Tagged Ports</h2>
<div id="tPorts"><button type="button" style="transform: translateY(-100%);margin: 0 50px 0 0" onclick="utClicked(true);" data-i18n="vlan_select_all">Select all</button></div>
<h2 data-i18n="vlan_untagged">Untagged Ports</h2>
<div id="uPorts"><button type="button" style="transform: translateY(-100%); margin: 0 50px 0 0" onclick="utClicked(false);" data-i18n="vlan_select_all">Select all</button> </div>
<h2 data-i18n="vlan_pvid">Use as default VLAN for incoming traffic (PVID)</h2>
<div id="pPorts"><button type="button" style="transform: translateY(-100%); margin: 0 50px 0 0" onclick="pvClicked(true);" data-i18n="vlan_select_all">Select all</button> </div>
<br/> <input style="width:40%;" class="action" id="vlan_sub" onclick="vlanSub();" type="button" data-i18n="vlan_update" value="Update / Create">
</form>
<h2 data-i18n="vlan_configured">Configured VLANs</h2>
<table id="vlanTable" style="width:90%">
<thead>
<tr>
<th>VLAN</th>
<th data-i18n="vlan_col_name">Name</th>
<th data-i18n="vlan_col_member">Member Ports</th>
<th data-i18n="vlan_col_tagged">Tagged Ports</th>
<th data-i18n="vlan_col_untagged">Untagged Ports</th>
<th data-i18n="vlan_col_pvid">PVID Ports</th>
<th data-i18n="vlan_col_delete">Delete</th>
</tr>
</thead>
<tbody id="vlanTableBody">
</tbody>
</table>
</div>
</body>
</html>
+32 -4
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Safe minifier for the WebUI sources (HTML/JS). """Safe minifier for the WebUI sources (HTML/JS).
Removes comments and line-leading whitespace without touching string Removes comments and line-leading whitespace without touching string or
literals, so the output is functionally identical to the input. Used regex literals, so the output is functionally identical to the input.
by the firmware Makefile before the files are embedded in the flash; Used by the firmware Makefile before the files are embedded in the
the raw sources in html/ stay untouched for development. flash; the raw sources in html/ stay untouched for development.
Binary files (.ico) are copied through unchanged. Binary files (.ico) are copied through unchanged.
@@ -19,6 +19,10 @@ def js_min(s):
out = [] out = []
i = 0 i = 0
n = len(s) n = len(s)
prev = '\n'
# Characters after which a '/' starts a regex literal rather than a
# division: operator / delimiter / statement-start contexts.
regex_start = set('([{=,:;!&|?+-*%^~<>')
while i < n: while i < n:
c = s[i] c = s[i]
if c == '"' or c == "'": if c == '"' or c == "'":
@@ -29,6 +33,28 @@ def js_min(s):
j += 1 j += 1
out.append(s[i:j + 1]) out.append(s[i:j + 1])
i = j + 1 i = j + 1
prev = c
elif c == '/' and (i + 1 >= n or s[i + 1] not in '/=*') and \
(prev in regex_start or prev == '\n'):
# Regex literal (e.g. /^#\// or /https?:\/\/x/g): copy through
# the closing '/', honouring escapes and character classes.
j = i + 1
in_class = False
while j < n:
ch = s[j]
if ch == '\\':
j += 2
continue
if ch == '[':
in_class = True
elif ch == ']':
in_class = False
elif ch == '/' and not in_class:
break
j += 1
out.append(s[i:j + 1])
i = j + 1
prev = '/'
elif c == '/' and i + 1 < n and s[i + 1] == '/': elif c == '/' and i + 1 < n and s[i + 1] == '/':
j = s.find('\n', i) j = s.find('\n', i)
i = j if j >= 0 else n i = j if j >= 0 else n
@@ -38,6 +64,8 @@ def js_min(s):
else: else:
out.append(c) out.append(c)
i += 1 i += 1
if c not in ' \t':
prev = c
lines = [l.strip() for l in ''.join(out).split('\n')] lines = [l.strip() for l in ''.join(out).split('\n')]
return '\n'.join(l for l in lines if l) + '\n' return '\n'.join(l for l in lines if l) + '\n'