Skip to content

Instantly share code, notes, and snippets.

@Podbrushkin
Last active June 6, 2025 11:22
Show Gist options
  • Save Podbrushkin/8945794a909da8176bbd517078a5e74a to your computer and use it in GitHub Desktop.
Save Podbrushkin/8945794a909da8176bbd517078a5e74a to your computer and use it in GitHub Desktop.
Format-HtmlTable
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="https://unpkg.com/[email protected]/dist/css/tabulator.min.css" rel="stylesheet">
<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/js/tabulator.min.js"></script>
<style>
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
text-decoration: underline;
}
.tag-filter-section {
margin: 10px 0;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.tag-filter-header {
font-weight: bold;
margin-bottom: 5px;
}
.tag-filter-options {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.tag-filter-option {
display: flex;
align-items: center;
white-space: nowrap;
}
.tag-filter-count {
margin-left: 5px;
color: #666;
font-size: 0.9em;
}
.tag-filter-select {
padding: 5px;
border-radius: 4px;
border: 1px solid #ccc;
}
.control-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.row-count {
margin-left: auto;
color: #666;
font-size: 0.9em;
}
</style>
</head>
<body>
<details>
<summary>Details</summary>
<pre>%DESCRIPTION%</pre>
</details>
<div id="tagFiltersContainer"></div>
<div class="control-bar">
<select id="tagFilterSelect" class="tag-filter-select">
<option value="">Tag & Filter</option>
</select>
<label for="fSearch">Search</label>
<input type="text" id="fSearch" name="fSearch">
<label>%DATETIME%</label>
<div class="row-count" id="rowCount">0 rows</div>
</div>
<div id="example-table"></div>
<script>
// Columns to be interpreted as tags initially
var initialTagColumns = [];
var data =
//JSON_ARRAY_START
[{"path":"./col-table-depth-counts.html","tags":["catalogue-of-life","biology"],"numTag":[1,4,1],"ggl":"http://www.google.com/"},{"path":"./index.html","tags":[],"numTag":[4,2,4],"ggl":"http://www.google.com/"},{"path":"./table-gbif-taxon-10k-byDescendants.html","tags":["table","biology","gbif"],"numTag":[3,0,2],"ggl":"http://www.google.com/"},{"path":"./wikimediaCommons/index.html","tags":[],"numTag":[3,0,0],"ggl":"http://www.google.com/"},{"path":"./col-table-rank-counts.html","tags":["catalogue-of-life","biology"],"numTag":[1,1,2],"ggl":"http://www.google.com/"},{"path":"./iNatur-tree-pictures-nsk-noBirdsNoPlants-dpi27-565.svg","tags":["iNaturalist","biology"],"numTag":[1,2,1],"ggl":"http://www.google.com/"},{"path":"./gbif-tree-pictures-birds.pdf","tags":["gbif","biology"],"numTag":[2,0,4],"ggl":"http://www.google.com/"},{"path":"./wikimediaCommons/Russian_writers_depicted.html","tags":["table"],"numTag":[0,3,3],"ggl":"http://www.google.com/"},{"path":"./biology/graph2d_Ursidae_taxons.html","tags":["taxon","graph","biology"],"numTag":[4,1,1],"ggl":"http://www.google.com/"},{"path":"./biology/index.html","tags":["biology"],"numTag":[0,0,2],"ggl":"http://www.google.com/"},{"path":"./librivox/tableReadersLangsRus.html","tags":["table"],"numTag":[2,2,0],"ggl":"http://www.google.com/"},{"path":"./gbif-tree-pictures-noPlantsInsectsBirdsMammal-LR.pdf","tags":["gbif","biology"],"numTag":[3,4,0],"ggl":"http://www.google.com/"},{"path":"./biology/tableTaxonsPlantsKnown3k.html","tags":["table","taxon","biology"],"numTag":[0,3,1],"ggl":"http://www.google.com/"},{"path":"./iNatur-tree-pictures-nsk-birds-dpi27-225.svg","tags":["iNaturalist","biology"],"numTag":[1,3,3],"ggl":"http://www.google.com/"},{"path":"./librivox/tableAllSectionsRus.html","tags":["table"],"numTag":[4,3,1],"ggl":"http://www.google.com/"},{"path":"./col-tree-pictures-birds-TB-206.svg","tags":["catalogue-of-life","biology"],"numTag":[4,3,1],"ggl":"http://www.google.com/"},{"path":"./librivox/tableReadersLangsSeconds.html","tags":["table"],"numTag":[4,0,1],"ggl":"http://www.google.com/"},{"path":"./wikisource/index.html","tags":[],"numTag":[0,0,4],"ggl":"http://www.google.com/"},{"path":"./col-tree-pictures-noPlantsInsectsBirdsMammal-TB-259.svg","tags":["catalogue-of-life","biology"],"numTag":[1,0,4],"ggl":"http://www.google.com/"},{"path":"./biology/graph3d_Ursidae_taxons.html","tags":["taxon","graph","biology"],"numTag":[4,1,1],"ggl":"http://www.google.com/"}]
//JSON_ARRAY_END
;
// Track currently active tag filters
var activeTagFilters = {};
function matchAny(data, filterParams) {
const regex = RegExp(filterParams.value, 'i');
for (var key in data) {
if (regex.test(data[key])) return true;
}
return false;
}
function combinedFilter(data) {
// Apply search filter
const searchValue = document.getElementById("fSearch").value;
const searchMatch = searchValue ? matchAny(data, { value: searchValue }) : true;
if (!searchMatch) return false;
// Check tag filters - different groups use AND logic
for (const [column, selectedTags] of Object.entries(activeTagFilters)) {
if (selectedTags.size === 0) continue;
const cellValue = data[column];
if (cellValue === undefined || cellValue === null) return false;
const values = Array.isArray(cellValue) ? cellValue : [cellValue];
// Within same group, use AND logic - all selected tags must be present
let allTagsMatch = true;
for (const tag of selectedTags) {
if (!values.some(v => String(v) === tag)) {
allTagsMatch = false;
break;
}
}
if (!allTagsMatch) return false;
}
return true;
}
function updateRowCount() {
const visibleRows = table.getData(true).length;
const totalRows = table.getData().length;
document.getElementById("rowCount").textContent = `${visibleRows} of ${totalRows} rows`;
}
const input = document.getElementById("fSearch");
input.addEventListener("keyup", function() {
table.setFilter(combinedFilter);
});
function urlFormatter(cell, formatterParams, onRendered){
return "<a href='" + cell.getValue() + "' target='_blank'>" + cell.getValue() + "</a>";
}
function htmlFormatter(cell, formatterParams, onRendered) {
return cell.getValue();
}
var table = new Tabulator("#example-table", {
height:"90vh",
layout:"fitColumns",
data: data, // Start with empty data
autoColumns: true,
});
function applyColumnFormatters() {
let columns = table.getColumnDefinitions();
let dataTmp = table.getData();
for (let i = 0; i < columns.length; i++) {
let columnName = columns[i].field;
if (columnName === "url") {
columns[i].formatter = urlFormatter;
continue;
}
let sampleVal = dataTmp.map(x => x[columnName]).find(val => val !== null && val !== undefined && val !== '');
if (typeof sampleVal === "string" && sampleVal.startsWith("http")) {
columns[i].formatter = urlFormatter;
}
if (typeof sampleVal === "string" && sampleVal.startsWith("<a href")) {
columns[i].formatter = htmlFormatter;
}
}
table.setColumns(columns);
}
function createTagFilter(column) {
// Remove existing filter for this column if it exists
const existingFilter = document.getElementById(`tag-filter-${column}`);
if (existingFilter) {
existingFilter.remove();
delete activeTagFilters[column];
table.setFilter(combinedFilter);
return;
}
// Get all unique values in the column
const uniqueValues = new Set();
const allData = table.getData();
allData.forEach(row => {
const value = row[column];
if (value !== undefined && value !== null) {
if (Array.isArray(value)) {
value.forEach(v => uniqueValues.add(String(v)));
} else {
uniqueValues.add(String(value));
}
}
});
// Create filter UI
const filterSection = document.createElement('div');
filterSection.className = 'tag-filter-section';
filterSection.id = `tag-filter-${column}`;
const header = document.createElement('div');
header.className = 'tag-filter-header';
header.textContent = `Filter by ${column}:`;
filterSection.appendChild(header);
const optionsContainer = document.createElement('div');
optionsContainer.className = 'tag-filter-options';
const sortedValues = Array.from(uniqueValues).sort((a, b) => a.localeCompare(b));
sortedValues.forEach(value => {
const option = document.createElement('div');
option.className = 'tag-filter-option';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = value;
checkbox.id = `tag-${column}-${value.replace(/\s+/g, '-')}`;
checkbox.addEventListener('change', () => {
if (!activeTagFilters[column]) {
activeTagFilters[column] = new Set();
}
if (checkbox.checked) {
activeTagFilters[column].add(value);
} else {
activeTagFilters[column].delete(value);
if (activeTagFilters[column].size === 0) {
delete activeTagFilters[column];
}
}
table.setFilter(combinedFilter);
});
const label = document.createElement('label');
label.htmlFor = checkbox.id;
const labelText = document.createElement('span');
labelText.textContent = value;
const count = document.createElement('span');
count.className = 'tag-filter-count';
// Static count - doesn't update with filtering
count.textContent = `(${allData.filter(row => {
const cellValue = row[column];
if (cellValue === undefined || cellValue === null) return false;
const values = Array.isArray(cellValue) ? cellValue : [cellValue];
return values.some(v => String(v) === value);
}).length})`;
label.appendChild(labelText);
label.appendChild(count);
option.appendChild(checkbox);
option.appendChild(label);
optionsContainer.appendChild(option);
});
filterSection.appendChild(optionsContainer);
document.getElementById('tagFiltersContainer').appendChild(filterSection);
// Initialize empty filter for this column
activeTagFilters[column] = new Set();
}
// Initialize tag filter select
function initTagFilterSelect() {
const select = document.getElementById('tagFilterSelect');
select.innerHTML = '<option value="">Tag & Filter</option>';
const columns = table.getColumnDefinitions();
columns.forEach(col => {
if (col.field) {
const option = document.createElement('option');
option.value = col.field;
option.textContent = col.field;
select.appendChild(option);
}
});
select.addEventListener('change', () => {
if (select.value) {
createTagFilter(select.value);
select.value = ""; // Reset the select
}
});
}
// Initialize table
table.on("tableBuilt", function() {
applyColumnFormatters();
initTagFilterSelect();
initialTagColumns.forEach(column => {
if (table.getColumn(column)) {
createTagFilter(column);
}
});
});
// Update row count when data changes
table.on("dataFiltered", function(filters, rows){
document.getElementById("rowCount").textContent = `${rows.length} of ${table.getData().length} rows`;
});
</script>
</body>
</html>
function Format-HtmlTable {
param (
[Parameter(ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
$JsonOrObject,
[string]$Description,
$MergeLabelsAndUrls = $true,
[array]$GroupBy = @(),
[switch]$DataTree
)
begin {
$all = @()
}
process {
$all += $JsonOrObject
}
end {
$fullJson = $null
if ($all[0] -is [String]) {
$fullJson = $all -join ' '
} else {
if ($MergeLabelsAndUrls) {
# merge urls and labels
$labelPropNames = $all | % {$_.psobject.properties} | ? name -Like *Label | % name | select -unique
#Write-Host $labelPropNames
$all = $all | ConvertTo-Json -depth 9 | ConvertFrom-Json -depth 9
$all | % {
$obj = $_
$labelPropNames | % {
$labelPropName = $_
$urlPropName = $_.Replace('Label','')
$label = $obj.$labelPropName
$url = $obj.$urlPropName
if ($label) {
$obj.$urlPropName = "<a href='$url' target='_blank' style='color: black;'>$label</a>"
}
$obj.psobject.properties.remove($labelPropName)
}
}
}
$fullJson = $all | ConvertTo-Json -Depth 5 # | % {$_ -replace '": null\r?\n',""": """"`n"}
}
$templatePath = Join-Path $PSScriptRoot 'Format-HtmlTable.html'
$htmlContent = (cat $templatePath -raw).replace('%DESCRIPTION%',[System.Web.HttpUtility]::HtmlEncode($Description)) `
-replace '%DATETIME%',(Get-Date -Format "yyyy-MM-dd")
$htmlContent = Replace-StringBetweenAnchors $htmlContent '//JSON_ARRAY_START\r' '\s*//JSON_ARRAY_END' $fullJson
if ($GroupBy) {
$rplcmnt = (,$GroupBy | ConvertTo-Json -Compress)
$htmlContent = $htmlContent -replace '(initialTagColumns = ).*?\n',"`$1$rplcmnt`n"
}
$filename = "table$(Get-Date -Format FileDateTime).html"
$outFile = $htmlContent | New-Item $filename
# Start $outFile
return $outFile
}
}
function Replace-StringBetweenAnchors ($inputString, $anchorLeft, $anchorRight, $replacementString) {
return ($inputString -replace "(?smi)(?<=$anchorLeft).*?(?=$anchorRight)", $replacementString)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment