271 lines
8.1 KiB
JavaScript
271 lines
8.1 KiB
JavaScript
const fs = require('fs')
|
|
const path = require('path')
|
|
const yaml = require('js-yaml')
|
|
const matter = require('gray-matter')
|
|
|
|
function sanitizeName(file) {
|
|
return file
|
|
.toLocaleLowerCase()
|
|
.replace(/\s+/g, '-')
|
|
.replace('---', '-')
|
|
.replace('--', '-')
|
|
.replace("'", '-')
|
|
}
|
|
|
|
// Function to evaluate filters recursively
|
|
function evaluateFilters(filters, fileObj, mdFile) {
|
|
if (filters.and) {
|
|
return filters.and.every((filter) =>
|
|
evaluateFilter(filter, fileObj, mdFile)
|
|
)
|
|
} else if (filters.or) {
|
|
return filters.or.some((filter) => evaluateFilter(filter, fileObj, mdFile))
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Function to evaluate a single filter (can be string or nested object)
|
|
function evaluateFilter(filter, fileObj, mdFile) {
|
|
if (typeof filter === 'string') {
|
|
return evaluateFilterString(filter, fileObj, mdFile)
|
|
} else if (typeof filter === 'object' && filter !== null) {
|
|
// Handle nested and/or conditions
|
|
return evaluateFilters(filter, fileObj, mdFile)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Function to evaluate a filter string
|
|
function evaluateFilterString(filter, fileObj, mdFile) {
|
|
try {
|
|
const filterFn = new Function(
|
|
'fileObj',
|
|
`
|
|
// File properties
|
|
const file = {
|
|
name: {
|
|
toString: () => String(fileObj['file.name'] || ''),
|
|
valueOf: () => String(fileObj['file.name'] || ''),
|
|
contains: (substring) => String(fileObj['file.name'] || '').includes(substring),
|
|
endsWith: (suffix) => String(fileObj['file.name'] || '').endsWith(suffix),
|
|
startsWith: (prefix) => String(fileObj['file.name'] || '').startsWith(prefix)
|
|
},
|
|
ext: fileObj['file.ext'],
|
|
tags: fileObj['tags'] || []
|
|
};
|
|
|
|
// Create safe getter function
|
|
const get = (prop) => fileObj[prop] || '';
|
|
|
|
// Create objects with methods for properties
|
|
const createPropertyObject = (value) => {
|
|
const val = value || '';
|
|
return {
|
|
toString: () => val,
|
|
valueOf: () => val,
|
|
isEmpty: () => !val || String(val).trim() === '',
|
|
contains: (substring) => String(val).includes(substring)
|
|
};
|
|
};
|
|
|
|
// Create variables for common properties that might be used in filters
|
|
const section = createPropertyObject(get('section'));
|
|
const kind = createPropertyObject(get('kind'));
|
|
const tags = get('tags') || [];
|
|
const title = createPropertyObject(get('title'));
|
|
const status = createPropertyObject(get('status'));
|
|
const type = createPropertyObject(get('type'));
|
|
const category = createPropertyObject(get('category'));
|
|
const priority = createPropertyObject(get('priority'));
|
|
const date = createPropertyObject(get('date'));
|
|
const author = createPropertyObject(get('author'));
|
|
const lang = createPropertyObject(get('lang'));
|
|
const website = createPropertyObject(get('website'));
|
|
const repository = createPropertyObject(get('repository'));
|
|
const draft = fileObj['draft'] === true || fileObj['draft'] === 'true';
|
|
|
|
// For equality comparisons, we need to handle the object wrapper
|
|
const originalSection = section;
|
|
const originalKind = kind;
|
|
|
|
// Override valueOf for comparisons to work
|
|
section.valueOf = () => get('section');
|
|
kind.valueOf = () => get('kind');
|
|
|
|
return ${filter};
|
|
`
|
|
)
|
|
return filterFn(fileObj)
|
|
} catch (error) {
|
|
console.error(
|
|
`Error evaluating filter '${filter}' for file '${mdFile}':`,
|
|
error
|
|
)
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Get all .md files in the current directory
|
|
const mdFiles = fs
|
|
.readdirSync('.')
|
|
.filter((file) => file.endsWith('.md') && !file.endsWith('.base.md'))
|
|
|
|
// Get all .base files in the bases directory
|
|
const baseFiles = fs
|
|
.readdirSync('./bases')
|
|
.filter((file) => file.endsWith('.base'))
|
|
|
|
// Process each base file
|
|
baseFiles.forEach((baseFile) => {
|
|
// Read the base file
|
|
const baseContent = fs.readFileSync(path.join('./bases', baseFile), 'utf8')
|
|
const baseData = yaml.load(baseContent)
|
|
|
|
// Get the base name without extension
|
|
const baseName = path.basename(baseFile, '.base')
|
|
|
|
// Process each view in the base file
|
|
if (!baseData || !baseData.views) {
|
|
console.warn(`Skipping "${baseFile}": no views defined`)
|
|
return
|
|
}
|
|
baseData.views.forEach((view) => {
|
|
// Create a list of objects with attributes from markdown files
|
|
const tableData = []
|
|
|
|
// Process each markdown file
|
|
mdFiles.forEach((mdFile) => {
|
|
// Read the markdown file
|
|
const mdContent = fs.readFileSync(mdFile, 'utf8')
|
|
const { data: frontmatter } = matter(mdContent)
|
|
|
|
// Create an object with file attributes
|
|
const fileObj = {
|
|
'file.name': path.basename(mdFile, '.md'),
|
|
'file.ext': 'md',
|
|
...frontmatter,
|
|
}
|
|
|
|
// Check if the file matches the filters
|
|
let matches = true
|
|
|
|
// Handle filters (evaluate the filter strings as JavaScript)
|
|
if (view.filters) {
|
|
matches = evaluateFilters(view.filters, fileObj, mdFile)
|
|
}
|
|
|
|
if (matches) {
|
|
// Add the file object to the table data
|
|
tableData.push(fileObj)
|
|
}
|
|
})
|
|
|
|
// Sort the table data
|
|
if (view.sort) {
|
|
view.sort.forEach((sortRule) => {
|
|
const property = sortRule.property
|
|
const direction = sortRule.direction || 'ASC'
|
|
|
|
tableData.sort((a, b) => {
|
|
let aVal = a[property]
|
|
let bVal = b[property]
|
|
|
|
// Convert values to strings for comparison
|
|
if (Array.isArray(aVal)) {
|
|
aVal = aVal.join(', ')
|
|
} else {
|
|
aVal = String(aVal || '')
|
|
}
|
|
|
|
if (Array.isArray(bVal)) {
|
|
bVal = bVal.join(', ')
|
|
} else {
|
|
bVal = String(bVal || '')
|
|
}
|
|
|
|
if (direction === 'DESC') {
|
|
return bVal.localeCompare(aVal)
|
|
} else {
|
|
return aVal.localeCompare(bVal)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
// Get the column names from the order array or from the data
|
|
let columnNames = view.order || []
|
|
if (columnNames.length === 0 && tableData.length > 0) {
|
|
columnNames = Object.keys(tableData[0])
|
|
}
|
|
|
|
// Generate output based on view type
|
|
let output = `---
|
|
list: ${baseName}
|
|
kind: reference
|
|
section: bases
|
|
---
|
|
|
|
`
|
|
|
|
if (view.type === 'cards') {
|
|
// Generate cards view as CSS grid with 3 columns
|
|
output +=
|
|
'<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; margin: 2rem 0;">\n'
|
|
|
|
tableData.forEach((row) => {
|
|
const fileName = row['file.name']
|
|
|
|
output += `<div style="border: 1px solid #eaecef; padding: 1.5rem;">\n`
|
|
output += `<strong><a href="${sanitizeName(fileName)}.html">${fileName}</a></strong><br/><br/>\n`
|
|
|
|
// Dynamically add all fields from order (except file.name)
|
|
columnNames.forEach((column) => {
|
|
if (column !== 'file.name') {
|
|
const value = row[column]
|
|
if (value) {
|
|
const displayValue = Array.isArray(value)
|
|
? value.join(', ')
|
|
: value
|
|
output += `<strong>${column}:</strong> ${displayValue}<br/>\n`
|
|
}
|
|
}
|
|
})
|
|
|
|
output += `</div>\n`
|
|
})
|
|
|
|
output += '</div>\n\n'
|
|
} else {
|
|
// Generate table view (default)
|
|
// Add the header row
|
|
output += `| ${columnNames.join(' | ')} |
|
|
`
|
|
output += `| ${columnNames.map(() => '---').join(' | ')} |
|
|
`
|
|
|
|
// Add the data rows
|
|
tableData.forEach((row) => {
|
|
const rowValues = columnNames.map((column) => {
|
|
const value = row[column]
|
|
if (column === 'file.name') {
|
|
// Create a link for file.name
|
|
return `[[${value}]]`
|
|
} else if (Array.isArray(value)) {
|
|
// Join array values
|
|
return value.join(', ')
|
|
} else {
|
|
// Return the value as string or empty string if undefined
|
|
return value || ''
|
|
}
|
|
})
|
|
output += `| ${rowValues.join(' | ')} |
|
|
`
|
|
})
|
|
}
|
|
|
|
// Write the output to a file
|
|
const outputFileName = `${baseName}.md`
|
|
fs.writeFileSync(outputFileName, output)
|
|
console.log(`Generated "${outputFileName}"`)
|
|
})
|
|
}) |