Module:MasonryLayout
Appearance
Documentation for this module may be created at Module:MasonryLayout/doc
--[[
* MasonryLayout.lua
* Intelligent masonry layout system for content distribution
*
* This module provides smart card distribution across columns for optimal visual balance.
* Integrates with the Blueprint architecture and follows ICANNWiki performance patterns.
*
* Key Features:
* - Content-aware size estimation
* - Intelligent card distribution algorithm
* - Responsive column management
* - Aggressive caching for performance
* - Blueprint integration
* - Error handling integration
*
* Integration with other modules:
* - ErrorHandling: All operations are protected with centralized error handling
* - TemplateHelpers: Uses caching mechanisms and utility functions
* - TemplateStructure: Integrates with block-based rendering
]]
local p = {}
-- ========== Constants as Upvalues ==========
local EMPTY_STRING = ''
local DEFAULT_COLUMNS = 3
local MOBILE_BREAKPOINT = 656 -- 41rem to match CSS breakpoint
-- Size estimation constants (based on typical MediaWiki table rendering)
local SIZE_ESTIMATES = {
BASE_CARD_HEIGHT = 120, -- Base table overhead (header + borders + padding)
HEADER_HEIGHT = 40, -- Table header height
ROW_HEIGHT = 25, -- Average height per table row
TITLE_CHAR_FACTOR = 1.2, -- Additional height for long titles
CONTENT_OVERFLOW_FACTOR = 15, -- Extra height for content overflow
MIN_CARD_HEIGHT = 80, -- Minimum card height
MAX_CARD_HEIGHT = 800 -- Maximum card height (safety limit)
}
-- Column distribution weights for different screen sizes
local RESPONSIVE_CONFIG = {
desktop = { columns = 3, minWidth = MOBILE_BREAKPOINT + 1 },
mobile = { columns = 1, maxWidth = MOBILE_BREAKPOINT }
}
-- ========== Required modules ==========
local ErrorHandling = require('Module:ErrorHandling')
local TemplateHelpers = require('Module:TemplateHelpers')
local NormalizationText = require('Module:NormalizationText')
-- ========== Module-level Caches ==========
local sizeCache = {}
local distributionCache = {}
-- ========== Utility Functions ==========
-- Generate a signature for a set of cards to use in cache keys
-- @param cards table Array of card objects
-- @return string A signature representing the card set
local function generateCardSignature(cards)
if not cards or #cards == 0 then
return 'empty'
end
local parts = {}
for i, card in ipairs(cards) do
parts[i] = string.format('%s:%d:%s',
card.id or 'unknown',
card.estimatedSize or 0,
card.contentType or 'default'
)
end
return table.concat(parts, '|')
end
-- Calculate balance score for column heights (lower is better)
-- @param columnHeights table Array of column heights
-- @return number Balance score (0 = perfect balance)
local function calculateBalance(columnHeights)
if not columnHeights or #columnHeights < 2 then
return 0
end
local total = 0
local count = #columnHeights
-- Calculate average height
for _, height in ipairs(columnHeights) do
total = total + height
end
local average = total / count
-- Calculate variance from average
local variance = 0
for _, height in ipairs(columnHeights) do
local diff = height - average
variance = variance + (diff * diff)
end
return variance / count
end
-- ========== Core Size Estimation ==========
-- Estimate the rendered height of a card based on its content
-- @param cardData table Card information: {title, rowCount, contentType, hasLongNames, isEmpty, rawDataCount}
-- @param options table Options: {mobileMode, customFactors}
-- @return number Estimated height in pixels
function p.estimateCardSize(cardData, options)
options = options or {}
-- Handle empty cards
if cardData.isEmpty then
return 0
end
-- Use raw data count if available for most accurate estimation
local effectiveRowCount = cardData.rawDataCount or cardData.rowCount or 0
-- Generate cache key
local cacheKey = TemplateHelpers.generateCacheKey(
'masonryCardSize',
cardData.title or '',
effectiveRowCount,
cardData.contentType or 'default',
cardData.hasLongNames and 'longNames' or 'shortNames',
options.mobileMode and 'mobile' or 'desktop'
)
return TemplateHelpers.withCache(cacheKey, function()
-- Card padding constant (15px top + 15px bottom from CSS)
local CARD_PADDING = 30
-- Special handling for intro cards (text content, not tables)
if cardData.contentType == 'intro' then
-- Intro is typically 2-3 lines of welcome text + card padding
return 120 + CARD_PADDING -- Fixed height for consistency
end
-- Special handling for infoBox
if cardData.contentType == 'infoBox' then
-- InfoBox has fixed structure: header + 4-5 rows + card padding
return 200 + CARD_PADDING -- Fixed height based on actual structure
end
-- Precise calculation for data tables
-- Based on actual MediaWiki table rendering measurements:
local TABLE_HEADER = 45 -- Table header with title
local TABLE_PADDING = 20 -- Top/bottom padding
local ROW_HEIGHT = 28 -- Actual height per row in pixels
local BORDER_SPACING = 2 -- Border between rows
-- Calculate exact height based on row count
local contentHeight = TABLE_HEADER + TABLE_PADDING + CARD_PADDING
if effectiveRowCount > 0 then
-- Each row takes ROW_HEIGHT plus border spacing
contentHeight = contentHeight + (effectiveRowCount * (ROW_HEIGHT + BORDER_SPACING))
-- Add extra height for long content names (wrapping)
if cardData.hasLongNames then
-- Long names typically wrap to 2 lines
local wrapBonus = math.floor(effectiveRowCount * 0.3) * ROW_HEIGHT
contentHeight = contentHeight + wrapBonus
end
end
-- Add small buffer for margin/padding variations
contentHeight = contentHeight + 15
-- Mobile adjustments (slightly more spacing)
if options.mobileMode then
contentHeight = contentHeight * 1.1
end
-- Apply reasonable bounds
contentHeight = math.max(80, contentHeight) -- Minimum height
contentHeight = math.min(2000, contentHeight) -- Maximum height (very large tables)
return math.floor(contentHeight)
end)
end
-- Analyze SMW query results to estimate card content
-- @param queryResults table Results from SMW query
-- @param cardType string Type of card ('organizations', 'people', etc.)
-- @return table Card data for size estimation
function p.analyzeQueryResults(queryResults, cardType)
if not queryResults or #queryResults == 0 then
return {
isEmpty = true,
rowCount = 0,
contentType = cardType
}
end
local rowCount = #queryResults
local hasLongNames = false
local totalNameLength = 0
-- Analyze content for long names
for _, result in ipairs(queryResults) do
local name = result.result or result[1] or ''
totalNameLength = totalNameLength + #name
if #name > 30 then
hasLongNames = true
end
end
local averageNameLength = totalNameLength / rowCount
return {
isEmpty = false,
rowCount = rowCount,
contentType = cardType,
hasLongNames = hasLongNames or averageNameLength > 25,
averageNameLength = averageNameLength
}
end
-- ========== Smart Distribution Algorithm ==========
-- Distribute cards across columns for optimal balance
-- @param cards table Array of card objects with size estimates
-- @param columnCount number Target number of columns (default: 3)
-- @return table Distribution result: {columns, heights, balance}
function p.distributeCards(cards, columnCount)
columnCount = columnCount or DEFAULT_COLUMNS
-- Handle edge cases
if not cards or #cards == 0 then
local emptyColumns = {}
local emptyHeights = {}
for i = 1, columnCount do
emptyColumns[i] = {}
emptyHeights[i] = 0
end
return {
columns = emptyColumns,
heights = emptyHeights,
balance = 0
}
end
-- Generate cache key
local cardSignature = generateCardSignature(cards)
local cacheKey = TemplateHelpers.generateCacheKey('masonryDistribution', cardSignature, columnCount)
return TemplateHelpers.withCache(cacheKey, function()
return p.distributeCardsIntelligent(cards, columnCount)
end)
end
-- Intelligent card distribution with balance optimization
-- @param cards table Array of card objects with size estimates
-- @param columnCount number Target number of columns
-- @return table Distribution result: {columns, heights, balance}
function p.distributeCardsIntelligent(cards, columnCount)
-- Pre-allocate column arrays
local columns = {}
local columnHeights = {}
for i = 1, columnCount do
columns[i] = {}
columnHeights[i] = 0
end
-- Separate special positioning cards from regular cards
local regularCards = {}
local infoBoxCard = nil
local introCard = nil
for _, card in ipairs(cards) do
if card.id == 'infoBox' then
infoBoxCard = card
elseif card.id == 'intro' then
introCard = card
else
table.insert(regularCards, card)
end
end
-- Place intro and infoBox first for alignment
if introCard then
table.insert(columns[1], introCard)
columnHeights[1] = introCard.estimatedSize or 0
end
if infoBoxCard then
table.insert(columns[columnCount], infoBoxCard)
columnHeights[columnCount] = infoBoxCard.estimatedSize or 0
end
-- Calculate total size and identify extreme cases
local totalRegularSize = 0
local cardSizes = {}
for _, card in ipairs(regularCards) do
local size = card.estimatedSize or 0
totalRegularSize = totalRegularSize + size
table.insert(cardSizes, {card = card, size = size})
end
-- Sort by size descending for optimal packing
table.sort(cardSizes, function(a, b) return a.size > b.size end)
-- Identify extremely large cards (using actual pixel calculations)
local extremeCards = {}
local normalCards = {}
local extremeThreshold = totalRegularSize * 0.4 -- 40% of total content
for _, item in ipairs(cardSizes) do
if item.size > extremeThreshold and #extremeCards < columnCount then
table.insert(extremeCards, item.card)
else
table.insert(normalCards, item.card)
end
end
-- Phase 1: Distribute extreme cards to minimize imbalance
if #extremeCards > 0 then
-- Find columns with least content (excluding special cards initially)
local availableColumns = {}
for i = 1, columnCount do
-- Skip columns with special cards for initial extreme card placement
if not ((i == 1 and introCard) or (i == columnCount and infoBoxCard)) then
table.insert(availableColumns, {column = i, height = columnHeights[i]})
end
end
-- If all columns have special cards, include them
if #availableColumns == 0 then
for i = 1, columnCount do
table.insert(availableColumns, {column = i, height = columnHeights[i]})
end
end
-- Sort by height ascending
table.sort(availableColumns, function(a, b) return a.height < b.height end)
-- Place extreme cards in shortest columns
for i, extremeCard in ipairs(extremeCards) do
if availableColumns[i] then
local targetColumn = availableColumns[i].column
table.insert(columns[targetColumn], extremeCard)
columnHeights[targetColumn] = columnHeights[targetColumn] + (extremeCard.estimatedSize or 0)
end
end
end
-- Phase 2: Distribute remaining cards using modified best-fit algorithm
for _, card in ipairs(normalCards) do
-- Calculate which column would result in best overall balance
local bestColumn = 1
local bestScore = math.huge
for i = 1, columnCount do
-- Simulate adding this card to column i
local simulatedHeight = columnHeights[i] + (card.estimatedSize or 0)
-- Calculate variance if we add to this column
local variance = 0
local totalHeight = 0
for j = 1, columnCount do
local h = (j == i) and simulatedHeight or columnHeights[j]
totalHeight = totalHeight + h
end
local avgHeight = totalHeight / columnCount
for j = 1, columnCount do
local h = (j == i) and simulatedHeight or columnHeights[j]
local diff = h - avgHeight
variance = variance + (diff * diff)
end
-- Prefer columns that minimize variance
if variance < bestScore then
bestScore = variance
bestColumn = i
end
end
-- Place card in best column
table.insert(columns[bestColumn], card)
columnHeights[bestColumn] = columnHeights[bestColumn] + (card.estimatedSize or 0)
end
-- Phase 3: Post-optimization - try to swap cards between columns to improve balance
-- This is a simple optimization pass that can significantly improve results
local improved = true
local iterations = 0
local maxIterations = 10
while improved and iterations < maxIterations do
improved = false
iterations = iterations + 1
-- Find most and least loaded columns
local maxCol, minCol = 1, 1
local maxHeight, minHeight = columnHeights[1], columnHeights[1]
for i = 2, columnCount do
if columnHeights[i] > maxHeight then
maxHeight = columnHeights[i]
maxCol = i
end
if columnHeights[i] < minHeight then
minHeight = columnHeights[i]
minCol = i
end
end
-- Try to move a card from max to min column
if maxCol ~= minCol and maxHeight - minHeight > 100 then -- Significant imbalance
-- Find a card in maxCol that would improve balance
for i = #columns[maxCol], 1, -1 do
local card = columns[maxCol][i]
-- Skip special cards
if card.id ~= 'intro' and card.id ~= 'infoBox' then
local cardSize = card.estimatedSize or 0
-- Check if moving this card would improve balance
local newMaxHeight = maxHeight - cardSize
local newMinHeight = minHeight + cardSize
if math.abs(newMaxHeight - newMinHeight) < math.abs(maxHeight - minHeight) then
-- Move the card
table.remove(columns[maxCol], i)
table.insert(columns[minCol], card)
columnHeights[maxCol] = newMaxHeight
columnHeights[minCol] = newMinHeight
improved = true
break
end
end
end
end
end
return {
columns = columns,
heights = columnHeights,
balance = calculateBalance(columnHeights),
totalSize = totalRegularSize,
extremeCards = #extremeCards,
iterations = iterations
}
end
-- ========== Blueprint Integration ==========
-- Create Blueprint-compatible block functions for masonry layout
-- @param cardDefinitions table Array of card definitions
-- @param options table Layout options
-- @return table Blueprint block configuration
function p.createMasonryBlocks(cardDefinitions, options)
options = options or {}
return {
masonryWrapperOpen = {
feature = 'fullPage',
render = function(template, args)
return p.renderMasonryOpen(options)
end
},
masonryContent = {
feature = 'fullPage',
render = function(template, args)
return p.renderMasonryContent(template, args, cardDefinitions, options)
end
},
masonryWrapperClose = {
feature = 'fullPage',
render = function(template, args)
return p.renderMasonryClose()
end
}
}
end
-- Render the opening masonry container
-- @param options table Layout options
-- @return string HTML for masonry container opening
function p.renderMasonryOpen(options)
options = options or {}
local cssClass = options.containerClass or 'country-hub-masonry-container'
return string.format('<div class="%s">', cssClass)
end
-- Render the closing masonry container
-- @return string HTML for masonry container closing
function p.renderMasonryClose()
return '</div>'
end
-- Render the main masonry content with intelligent distribution
-- @param template table Template object
-- @param args table Template arguments
-- @param cardDefinitions table Array of card definitions
-- @param options table Layout options
-- @return string Complete masonry layout HTML
function p.renderMasonryContent(template, args, cardDefinitions, options)
if not template._errorContext then
template._errorContext = ErrorHandling.createContext("MasonryLayout")
end
return ErrorHandling.protect(
template._errorContext,
"renderMasonryContent",
function()
return p.renderMasonryContentInternal(template, args, cardDefinitions, options)
end,
EMPTY_STRING,
template, args, cardDefinitions, options
)
end
-- Internal masonry content rendering (protected by error handling)
-- @param template table Template object
-- @param args table Template arguments
-- @param cardDefinitions table Array of card definitions
-- @param options table Layout options
-- @return string Complete masonry layout HTML
function p.renderMasonryContentInternal(template, args, cardDefinitions, options)
options = options or {}
local columnCount = options.columns or DEFAULT_COLUMNS
-- Build cards with content analysis
local cards = {}
local cardIndex = 1
for _, cardDef in ipairs(cardDefinitions) do
-- Check if this card's feature is enabled
if template.features[cardDef.feature] then
-- Get the block renderer for this card
local renderer = template._blocks and template._blocks[cardDef.blockId]
if renderer and renderer.render then
-- Render the card content
local cardContent = renderer.render(template, args)
if cardContent and cardContent ~= EMPTY_STRING then
-- Get raw data count if available
local rawDataCount = template._rawDataCounts and template._rawDataCounts[cardDef.blockId] or nil
-- Analyze the content for size estimation
local cardData = p.analyzeCardContent(cardContent, cardDef, rawDataCount)
cardData.id = cardDef.blockId
cardData.title = cardDef.title or cardDef.blockId
cardData.content = cardContent
cardData.estimatedSize = p.estimateCardSize(cardData, options)
cards[cardIndex] = cardData
cardIndex = cardIndex + 1
end
end
end
end
-- Distribute cards across columns
local distribution = p.distributeCards(cards, columnCount)
-- Render the distributed layout
return p.renderDistributedLayout(distribution, options)
end
-- Analyze rendered card content to extract size information
-- @param cardContent string Rendered HTML content
-- @param cardDef table Card definition
-- @param rawDataCount number Optional: actual count of data entries from SMW query
-- @return table Card data for size estimation
function p.analyzeCardContent(cardContent, cardDef, rawDataCount)
if not cardContent or cardContent == EMPTY_STRING then
return {
isEmpty = true,
rowCount = 0,
contentType = cardDef.blockId,
rawDataCount = 0
}
end
-- Use raw data count if provided (most accurate)
local rowCount = rawDataCount or 0
-- Fallback: Count table rows in the content (look for <tr> tags or table row patterns)
if rowCount == 0 then
local _, trCount = cardContent:gsub('<tr[^>]*>', '')
local _, wikiRowCount = cardContent:gsub('|-', '')
rowCount = math.max(trCount, wikiRowCount)
end
-- Final fallback: estimate based on content length
if rowCount == 0 and #cardContent > 100 then
-- Rough estimation: every ~200 characters might be a row
rowCount = math.ceil(#cardContent / 200)
end
-- Check for long content (rough heuristic)
local hasLongNames = #cardContent > 1000 or cardContent:find('[%w%s%-%.]+[%w%s%-%.]+[%w%s%-%.]+[%w%s%-%.]+[%w%s%-%.]+[%w%s%-%.]+')
return {
isEmpty = false,
rowCount = math.max(1, rowCount), -- At least 1 row if content exists
contentType = cardDef.blockId,
hasLongNames = hasLongNames,
contentLength = #cardContent,
rawDataCount = rawDataCount or rowCount -- Store the actual count for debugging
}
end
-- Render the final distributed layout
-- @param distribution table Distribution result from distributeCards
-- @param options table Layout options
-- @return string Complete HTML for the distributed layout
function p.renderDistributedLayout(distribution, options)
options = options or {}
local columnClass = options.columnClass or 'country-hub-masonry-column'
local cardClass = options.cardClass or 'country-hub-masonry-card'
local columns = distribution.columns
local columnCount = #columns
if columnCount == 0 then
return EMPTY_STRING
end
-- Build HTML for each column
local columnHtml = {}
for i = 1, columnCount do
local columnCards = columns[i]
local cardHtml = {}
-- Render cards in this column
for j, card in ipairs(columnCards) do
-- ALL cards should be wrapped in the card class for consistent styling
cardHtml[j] = string.format(
'<div class="%s" data-card-id="%s">%s</div>',
cardClass,
card.id or 'unknown',
card.content or EMPTY_STRING
)
end
-- Wrap column
columnHtml[i] = string.format(
'<div class="%s" data-column="%d">%s</div>',
columnClass,
i,
table.concat(cardHtml, '\n')
)
end
return table.concat(columnHtml, '\n')
end
-- ========== Responsive Utilities ==========
-- Get responsive column count based on screen size
-- @param screenWidth number Screen width in pixels
-- @return number Appropriate column count
function p.getResponsiveColumns(screenWidth)
if screenWidth <= MOBILE_BREAKPOINT then
return 1
else
return 3
end
end
-- Generate responsive CSS classes
-- @param options table Layout options
-- @return string CSS classes for responsive behavior
function p.generateResponsiveClasses(options)
options = options or {}
local baseClass = options.baseClass or 'country-hub-masonry'
local classes = {baseClass}
if options.mobileColumns then
table.insert(classes, baseClass .. '-mobile-' .. options.mobileColumns)
end
if options.tabletColumns then
table.insert(classes, baseClass .. '-tablet-' .. options.tabletColumns)
end
if options.desktopColumns then
table.insert(classes, baseClass .. '-desktop-' .. options.desktopColumns)
end
return table.concat(classes, ' ')
end
-- ========== Intelligent Layout Rendering ==========
-- Main render function for intelligent masonry layout
-- This is the core function that coordinates content rendering, analysis, and distribution
-- @param template table Template object with features and configuration
-- @param args table Template arguments
-- @param config table Configuration with cardDefinitions, options, and blockRenderers
-- @return string Complete masonry layout HTML
function p.renderIntelligentLayout(template, args, config)
-- Create render-time error context (Blueprint pattern)
local errorContext = ErrorHandling.createContext("MasonryLayout")
return ErrorHandling.protect(
errorContext,
"renderIntelligentLayout",
function()
return p.renderIntelligentLayoutInternal(template, args, config, errorContext)
end,
EMPTY_STRING,
template, args, config
)
end
-- Internal implementation of intelligent layout rendering
-- @param template table Template object
-- @param args table Template arguments
-- @param config table Configuration object
-- @param errorContext table Error context for protected operations
-- @return string Complete masonry layout HTML
function p.renderIntelligentLayoutInternal(template, args, config, errorContext)
local cardDefinitions = config.cardDefinitions or {}
local options = config.options or {}
local blockRenderers = config.blockRenderers or {}
-- Determine render mode (mobile vs desktop)
-- Default to desktop mode, but can be overridden by options
local isMobileMode = options.mobileMode or false
local columnCount = isMobileMode and 1 or (options.columns or DEFAULT_COLUMNS)
-- Build cards with render-time content generation
local cards = {}
local cardIndex = 1
for _, cardDef in ipairs(cardDefinitions) do
-- Check if this card's feature is enabled
if template.features[cardDef.feature] then
-- Get the block renderer for this card
local renderer = blockRenderers[cardDef.blockId]
if renderer and renderer.render then
-- Render the card content at render-time (not configuration-time)
local cardContent = ErrorHandling.protect(
errorContext,
"renderCard_" .. cardDef.blockId,
function()
return renderer.render(template, args)
end,
EMPTY_STRING,
template, args
)
if cardContent and cardContent ~= EMPTY_STRING then
-- Analyze the content for size estimation
local cardData = p.analyzeCardContent(cardContent, cardDef)
cardData.id = cardDef.blockId
cardData.title = cardDef.title or cardDef.blockId
cardData.content = cardContent
cardData.estimatedSize = p.estimateCardSize(cardData, options)
cards[cardIndex] = cardData
cardIndex = cardIndex + 1
end
end
end
end
-- Branch based on render mode
if isMobileMode or columnCount == 1 then
-- MOBILE MODE: Single column with explicit ordering
local orderedCards = {}
local introCard = nil
local infoBoxCard = nil
local otherCards = {}
-- Separate special cards from regular cards
for _, card in ipairs(cards) do
if card.id == 'intro' then
introCard = card
elseif card.id == 'infoBox' then
infoBoxCard = card
else
table.insert(otherCards, card)
end
end
-- Build final ordered list: intro → infoBox → others
if introCard then
table.insert(orderedCards, introCard)
end
if infoBoxCard then
table.insert(orderedCards, infoBoxCard)
end
for _, card in ipairs(otherCards) do
table.insert(orderedCards, card)
end
-- Render as single column layout
local containerClass = options.containerClass or 'country-hub-masonry-container'
local cardClass = options.cardClass or 'country-hub-masonry-card'
local masonryHtml = string.format('<div class="%s country-hub-mobile-mode">', containerClass)
-- Add debug information
masonryHtml = masonryHtml .. string.format(
'<!-- Masonry Debug: Total cards: %d, Mobile single column layout -->',
#orderedCards
)
-- Render each card in order
for _, card in ipairs(orderedCards) do
masonryHtml = masonryHtml .. string.format(
'\n<div class="%s" data-card-id="%s">%s</div>',
cardClass,
card.id or 'unknown',
card.content or EMPTY_STRING
)
end
masonryHtml = masonryHtml .. '\n</div>'
return masonryHtml
else
-- DESKTOP MODE: Multi-column with intelligent distribution
local distribution = p.distributeCards(cards, columnCount)
-- Render the complete masonry layout
local containerClass = options.containerClass or 'country-hub-masonry-container'
local masonryHtml = string.format('<div class="%s">', containerClass)
-- Add debug information as HTML comments
if distribution then
masonryHtml = masonryHtml .. string.format(
'<!-- Masonry Debug: Total cards: %d, Columns: %d, Heights: [%s], Balance: %.2f, Extreme cards: %d -->',
#cards,
#(distribution.columns or {}),
table.concat(distribution.heights or {}, ', '),
distribution.balance or 0,
distribution.extremeCards or 0
)
-- Add per-card debug info
for i, column in ipairs(distribution.columns or {}) do
masonryHtml = masonryHtml .. string.format('\n<!-- Column %d: ', i)
for _, card in ipairs(column) do
masonryHtml = masonryHtml .. string.format('%s(%dpx) ', card.id or 'unknown', card.estimatedSize or 0)
end
masonryHtml = masonryHtml .. '-->'
end
end
masonryHtml = masonryHtml .. '\n' .. p.renderDistributedLayout(distribution, options)
masonryHtml = masonryHtml .. '</div>'
return masonryHtml
end
end
-- ========== Debug and Utilities ==========
-- Get distribution statistics for debugging
-- @param distribution table Distribution result
-- @return table Statistics about the distribution
function p.getDistributionStats(distribution)
if not distribution or not distribution.columns then
return {
columnCount = 0,
totalCards = 0,
balance = 0,
heights = {}
}
end
local totalCards = 0
for _, column in ipairs(distribution.columns) do
totalCards = totalCards + #column
end
return {
columnCount = #distribution.columns,
totalCards = totalCards,
balance = distribution.balance or 0,
heights = distribution.heights or {},
averageHeight = distribution.heights and (#distribution.heights > 0) and
(table.concat(distribution.heights, '+') / #distribution.heights) or 0
}
end
-- Clear caches (for debugging/testing)
function p.clearCaches()
sizeCache = {}
distributionCache = {}
-- Also clear TemplateHelpers cache if needed
end
return p