standalone

This commit is contained in:
2026-05-28 01:25:53 +08:00
parent ca9cc62b8c
commit 16847a78ca
12 changed files with 859 additions and 443 deletions

View File

@@ -41,17 +41,26 @@ type mraidContext struct {
func MraidPage(w http.ResponseWriter, r *http.Request) {
body := `
<h2>MRAID Checker</h2>
<p class="hint">Static checks for AppLovin, ironSource, and Unity folder HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.</p>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">
<input id="folder" type="text" placeholder="Folder to scan recursively..." style="flex:1;" />
<button id="pickFolder">Browse Folder...</button>
<button id="pickFiles">Pick Files...</button>
<button id="scan">Scan</button>
</div>
<div id="selection" class="hint"></div>
<div id="status" style="margin-top:8px;"></div>
<div id="results" style="margin-top:8px;"></div>
<header class="tool-header">
<h2 class="tool-title">MRAID Checker</h2>
<p class="tool-description">Static checks for AppLovin, ironSource, and Unity folder HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.</p>
</header>
<section class="tool-panel">
<div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
<div class="panel-body">
<div class="control-group">
<div class="file-row">
<button id="pickFolder" class="secondary">Select Folder</button>
<button id="pickFiles" class="secondary">Select File(s)</button>
<button id="clear" class="secondary">Clear</button>
</div>
<div id="selection" class="selected-list"></div>
</div>
<div class="action-row"><button id="scan">Scan</button></div>
<div id="status" class="status-panel"></div>
</div>
</section>
<div id="results" class="results-panel"></div>
<style>
.row-result { padding:8px 0; border-bottom:1px solid #333; }
@@ -75,35 +84,73 @@ const folderEl = document.getElementById('folder');
const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');
const selectionEl = document.getElementById('selection');
const PAGE_SIZE = 5;
let pickedFiles = [];
function clearFiles(){ pickedFiles = []; selectionEl.textContent = ''; }
let selectedPage = 0;
function escapeText(value){ return String(value).replace(/[&<>"']/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch])); }
folderEl.addEventListener('input', clearFiles);
function basename(p) { const i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\')); return i >= 0 ? p.slice(i + 1) : p; }
function renderSelection() {
const oldPager = document.getElementById('selectedPager');
if (oldPager) oldPager.remove();
if (!pickedFiles.length) {
selectionEl.innerHTML = '<span class="file-name">(no files selected)</span>';
return;
}
const maxPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
selectedPage = Math.min(selectedPage, maxPage);
const start = selectedPage * PAGE_SIZE;
const visible = pickedFiles.slice(start, start + PAGE_SIZE);
selectionEl.innerHTML = '<div class="results-panel" style="margin-top:0;"><table class="data-table">' +
'<thead><tr><th>Filename</th><th style="width:44px;"></th></tr></thead><tbody>' +
visible.map((p, offset) => {
const i = start + offset;
return '<tr><td class="mono wrap">' + escapeText(basename(p)) + '</td><td><button class="remove-selected danger" data-index="' + i + '" title="Remove">×</button></td></tr>';
}).join('') + '</tbody></table></div>';
selectionEl.querySelectorAll('.remove-selected').forEach(btn => {
btn.addEventListener('click', () => { pickedFiles.splice(Number(btn.dataset.index), 1); renderSelection(); });
});
if (pickedFiles.length > PAGE_SIZE) {
const pager = document.createElement('div');
pager.id = 'selectedPager';
pager.className = 'pager';
pager.innerHTML = '<button class="secondary" id="prevPage"' + (selectedPage === 0 ? ' disabled' : '') + '>Previous</button>' +
'<span class="file-name">Page ' + (selectedPage + 1) + ' of ' + (maxPage + 1) + '</span>' +
'<button class="secondary" id="nextPage"' + (selectedPage === maxPage ? ' disabled' : '') + '>Next</button>';
selectionEl.appendChild(pager);
pager.querySelector('#prevPage').addEventListener('click', () => { selectedPage--; renderSelection(); });
pager.querySelector('#nextPage').addEventListener('click', () => { selectedPage++; renderSelection(); });
}
}
function addPaths(paths) {
const existing = new Set(pickedFiles);
pickedFiles = pickedFiles.concat((paths || []).filter(p => !existing.has(p)));
selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
renderSelection();
}
renderSelection();
document.getElementById('pickFolder').addEventListener('click', async () => {
const r = await fetch('/api/mraid/pickFolder', { method: 'POST' });
const j = await r.json();
if (j.path) { folderEl.value = j.path; clearFiles(); }
addPaths(j.paths || []);
});
document.getElementById('pickFiles').addEventListener('click', async () => {
const r = await fetch('/api/mraid/pickFiles', { method: 'POST' });
const j = await r.json();
if (j.paths && j.paths.length) {
pickedFiles = j.paths;
folderEl.value = '';
selectionEl.textContent = pickedFiles.length + ' file(s) selected';
}
addPaths(j.paths || []);
});
document.getElementById('clear').addEventListener('click', () => {
pickedFiles = [];
selectedPage = 0;
statusEl.textContent = '';
resultsEl.innerHTML = '';
renderSelection();
});
document.getElementById('scan').addEventListener('click', async () => {
statusEl.textContent = 'Scanning...';
resultsEl.innerHTML = '';
let payload;
if (pickedFiles.length) payload = { files: pickedFiles };
else {
const folder = folderEl.value.trim();
if (!folder) { statusEl.textContent = 'Pick a folder or files first.'; return; }
payload = { folder };
}
if (!pickedFiles.length) { statusEl.textContent = 'Pick files first.'; return; }
const payload = { files: pickedFiles };
const r = await fetch('/api/mraid/scan', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
const j = await r.json();
if (j.error) { statusEl.textContent = 'Error: ' + j.error; return; }
@@ -114,14 +161,17 @@ document.getElementById('scan').addEventListener('click', async () => {
const skipped = Number(j.skipped || 0);
const skippedReason = j.skippedReason || 'file(s).';
const skippedText = skipped ? ' Skipped ' + skipped + ' ' + skippedReason : '';
statusEl.innerHTML = '<div class="summary">Scanned ' + total + ' HTML file(s). ' + withIssues + ' file(s) need review. ' + requirements + ' requirement issue(s), ' + bestPractices + ' best-practice warning(s).' + skippedText + '</div>';
resultsEl.innerHTML = '';
statusEl.textContent = 'Scanned ' + total + ' HTML file(s). ' + withIssues + ' file(s) need review.' + skippedText;
if (!total) { resultsEl.innerHTML = ''; return; }
resultsEl.innerHTML = '<table class="data-table"><thead><tr><th>File</th><th>Results</th></tr></thead><tbody></tbody></table>';
const tbody = resultsEl.querySelector('tbody');
for (const rr of j.results) {
const reqCount = rr.issues.filter(i => i.severity === 'requirement').length;
const bpCount = rr.issues.filter(i => i.severity === 'best-practice').length;
const row = document.createElement('div');
row.className = 'row-result';
row.innerHTML = '<div class="file-head"><span class="mark ' + (rr.ok ? 'ok' : 'bad') + '">' + (rr.ok ? 'OK' : 'X') + '</span><span class="file-name">' + escapeText(rr.file) + '</span><span class="counts">' + reqCount + ' req / ' + bpCount + ' bp</span></div>';
const row = document.createElement('tr');
const file = document.createElement('td');
file.className = 'mono wrap';
file.textContent = rr.file;
const result = document.createElement('td');
result.className = 'wrap';
if (!rr.ok) {
const list = document.createElement('ul');
list.className = 'issues';
@@ -132,9 +182,12 @@ document.getElementById('scan').addEventListener('click', async () => {
item.innerHTML = '<span class="badge ' + i.severity + '">' + escapeText(i.severity) + '</span><span class="issue-title">' + escapeText(line + ' ' + i.title) + '</span><div class="issue-detail">' + escapeText(i.detail) + '</div>';
list.appendChild(item);
}
row.appendChild(list);
result.appendChild(list);
} else {
result.innerHTML = '<span class="badge ok">OK</span>';
}
resultsEl.appendChild(row);
row.append(file, result);
tbody.appendChild(row);
}
});
</script>
@@ -144,7 +197,18 @@ document.getElementById('scan').addEventListener('click', async () => {
}
func MraidPickFolder(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{"path": PickFolder("Select folder to scan", "")})
p := PickFolder("Select folder to scan", "")
if p == "" {
writeJSON(w, map[string]any{"paths": []string{}})
return
}
files := []string{}
for _, f := range collectHTMLFiles(p) {
if isInSupportedMraidFolderPath(f) {
files = append(files, f)
}
}
writeJSON(w, map[string]any{"paths": files})
}
func MraidPickFiles(w http.ResponseWriter, r *http.Request) {