Last active
April 21, 2022 18:56
-
-
Save bmcminn/61b4d8454fe36da11e428b011612947b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// dumb helpers functions | |
/** | |
* Generates a random integer between a min and max value | |
* @sauce https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values | |
* @param number min | |
* @param number max | |
* @return number | |
*/ | |
function randomInt(min, max) { | |
if (max < min) { | |
throw Error(`Arguemnt 'max:${max}' cannot be less than argument 'min:${min}'`) | |
} | |
min = Math.ceil(min); | |
max = Math.floor(max); | |
return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive | |
} | |
console.assert(randomInt(-10, 0) < 0) | |
console.assert(randomInt(-10, 0) < 0) | |
console.assert(randomInt(-10, 0) < 0) | |
console.assert(randomInt(-10, 0) < 0) | |
console.assert(randomInt(-10, 0) < 0) | |
console.assert(randomInt(-10, 0) < 0) | |
console.assert(randomInt(-10, 0) < 0) | |
console.assert(randomInt(-10, 0) < 0) | |
console.assert(randomInt(-10, 0) < 0) | |
console.assert(randomInt(-10, 0) < 0) | |
/** | |
* Generates a random integer between a min and max value | |
* @sauce https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values_inclusive | |
* @param number min | |
* @param number max | |
* @return number | |
*/ | |
function randomIntInclusive(min, max) { | |
if (max < min) { | |
throw Error(`Arguemnt 'max:${max}' cannot be less than argument 'min:${min}'`) | |
} | |
min = Math.ceil(min); | |
max = Math.floor(max); | |
return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive | |
} | |
console.assert(randomIntInclusive(-10, 0) <= 0) | |
console.assert(randomIntInclusive(-10, 0) <= 0) | |
console.assert(randomIntInclusive(-10, 0) <= 0) | |
console.assert(randomIntInclusive(-10, 0) <= 0) | |
console.assert(randomIntInclusive(-10, 0) <= 0) | |
console.assert(randomIntInclusive(-10, 0) <= 0) | |
console.assert(randomIntInclusive(-10, 0) <= 0) | |
console.assert(randomIntInclusive(-10, 0) <= 0) | |
console.assert(randomIntInclusive(-10, 0) <= 0) | |
console.assert(randomIntInclusive(-10, 0) <= 0) | |
/** | |
* Returns the XOR of two values | |
* @sauce https://www.howtocreate.co.uk/xor.html | |
* @param any a | |
* @param any b | |
* @return boolean | |
*/ | |
function xor(a, b) { | |
// return a != b || b != a | |
// return (a || b) && !(a && b) | |
return !a != !b | |
} | |
console.assert(xor(true, false) === true) | |
console.assert(xor(false, false) === false) | |
console.assert(xor(1,0) === true) | |
console.assert(xor(1,1) === false) | |
console.assert(xor(5, 3) === false) | |
console.assert(xor(5, 5) === false) | |
/** | |
* [unique description] | |
* @param {[type]} array [description] | |
* @return {[type]} [description] | |
*/ | |
function unique(array) { | |
return [...new Set(array)] | |
} | |
console.assert(unique([1,2,2,3,3,3]).toString() === '1,2,3') | |
/** | |
* [flatten description] | |
* @param array data [description] | |
* @return {[type]} [description] | |
*/ | |
function flattenSimple(data) { | |
return data.toString().split(',') | |
// NOTE: from here you can normalize the flattened array values however you wish using the .map() method | |
// EX: flatten([...]).map(el => Number(el)) | |
} | |
/** | |
* [flattenSimple description] | |
* @param {[type]} data [description] | |
* @return {[type]} [description] | |
*/ | |
function flatten(data, depth=5) { | |
const res = [] | |
data.forEach((value, index) => { | |
if (Array.isArray(value) && depth > 0) { | |
depth -= 1 | |
flatten(value, depth).forEach(entry => res.push(entry)) | |
return | |
} | |
res.push(value) | |
}) | |
return res | |
} | |
// function flattenObject(data, depth = 5) { | |
// let res = {} | |
// Object.keys(data).map((key) => { | |
// if (data.hasOwnProperty(key)) { | |
// let value = data[key] | |
// if (isObject(value) && depth > 0) { | |
// depth -= 1 | |
// value = flattenObject(value) | |
// } | |
// res[key] = value | |
// // res.push(`${key}:${value}`) | |
// } | |
// }) | |
// return res | |
// } | |
/** | |
* Determines if the values for A and B are identitical | |
* @param {any} a | |
* @param {any} b | |
* @param {boolean} strict Enforces to check if A and B are strictly identical | |
* @return {boolean} | |
*/ | |
function eq(a, b, strict = false) { | |
const isObject = (data) => data && Object.getPrototypeOf(data) === Object.prototype | |
const flattenObject = (data, depth = 5) => { | |
let res = [] | |
Object.keys(data).map((key) => { | |
if (data.hasOwnProperty(key)) { | |
let value = data[key] | |
if (isObject(value) && depth > 0) { | |
depth -= 1 | |
value = flattenObject(value) | |
} | |
res.push(`${key}:${value}`) | |
} | |
}) | |
return res | |
} | |
if (isObject(a) && !isObject(b)) { return false } | |
if (!isObject(a) && isObject(b)) { return false } | |
if (isObject(a) && isObject(b)) { | |
a = flattenObject(a) | |
b = flattenObject(b) | |
} | |
a = a.toString().split('') | |
b = b.toString().split('') | |
if (!strict) { | |
a = a.sort() | |
b = b.sort() | |
} | |
return a.toString() === b.toString() | |
} | |
console.assert(eq(1,1) === true) | |
console.assert(eq(1,0) === false) | |
console.assert(eq(true, true) === true) | |
console.assert(eq(true, false) === false) | |
console.assert(eq(false, false) === true) | |
console.assert(eq(false, false) === true) | |
console.assert(eq({ name: 'susan', age: 17 }, { name: 'susan' }) === false) | |
console.assert(eq({ name: 'susan', }, { name: 'susan' }) === true) | |
/** | |
* Returns N, provided N is within the given min/max range, else returns min or max accordingly | |
* @param number n | |
* @param number min | |
* @param number max | |
* @return number A number between min and max | |
*/ | |
function clamp(n, min, max) { | |
if (max < min) { | |
throw Error(`Arguemnt 'max:${max}' cannot be less than argument 'min:${min}'`) | |
} | |
if (n < min) { return min } | |
if (n > max) { return max } | |
return n | |
} | |
console.assert(clamp(-5, 0, 10) === 0) | |
console.assert(clamp(15, 0, 10) === 10) | |
/** | |
* Coverts a given index to the coordinates of a grid of `width`` | |
* @param int index [description] | |
* @param width width [description] | |
* @return array<[int x, int y]> | |
*/ | |
function indexToCoords(index, width) { | |
let x = index % width | |
let y = Math.floor(index / width) | |
return [x, y] | |
} | |
console.assert(indexToCoords(6, 4).toString() === '2,1') | |
/** | |
* [coordsToIndex description] | |
* @param integer x [description] | |
* @param integer y [description] | |
* @param integer width The number of indexes wide the grid is | |
* @return integer [description] | |
*/ | |
function coordsToIndex(x, y, width) { | |
return (y * width) + x | |
} | |
console.assert(coordsToIndex(2,1,4) === 6) | |
/** | |
* [isObject description] | |
* @param {[type]} data [description] | |
* @return {Boolean} [description] | |
*/ | |
function isObject(data) { | |
return data && Object.getPrototypeOf(data) === Object.prototype // && !Array.isArray(data) | |
} | |
console.assert(isObject({}) === true) | |
console.assert(isObject([{}]) === false) | |
/** | |
* [isArray description] | |
* @param {[type]} data [description] | |
* @return {Boolean} [description] | |
*/ | |
function isArray(data) { | |
return Array.isArray(data) | |
} | |
console.assert(isArray([]) === true) | |
console.assert(isArray([{}]) === true) | |
console.assert(isArray('123456') === false) | |
console.assert(isArray('sefjslef') === false) | |
console.assert(isArray(12345) === false) | |
/** | |
* Determins if a given value is "empty" or undefined in some way | |
* @param any data Data you wish to check for empty state | |
* @param boolean trim Trims the result to ensure strings are actually empty | |
* @return boolean | |
*/ | |
function isEmpty(data, trim = true) { | |
if (data && Object.getPrototypeOf(data) === Object.prototype) { | |
data = Object.keys(data) | |
} | |
let res = data ? data.toString() : '' | |
if (res && trim) { res = res.trim() } | |
return res.length === 0 | |
} | |
console.assert(isEmpty(NaN) === true, `test isEmpty(NaN)`) | |
console.assert(isEmpty(null) === true, `test isEmpty(null)`) | |
console.assert(isEmpty(undefined) === true, `test isEmpty(undefined)`) | |
console.assert(isEmpty(false) === true, `test isEmpty(false)`) | |
console.assert(isEmpty([]) === true, `test isEmpty([])`) | |
console.assert(isEmpty({}) === true, `test isEmpty({})`) | |
console.assert(isEmpty(' ') === true, `test isEmpty(' '`) | |
console.assert(isEmpty('') === true, `test isEmpty('')`) | |
console.assert(isEmpty(' ', false) === false, `test isEmpty(' '`) | |
console.assert(isEmpty(['waffles']) === false, `test isEmpty(['waffles'`) | |
console.assert(isEmpty({ waffles: null }) === false, `{waffles = null} is empty`) | |
console.assert(isEmpty(true) === false, `test isEmpty(true)`) | |
console.assert(isEmpty(123456) === false, `test isEmpty(123456)`) | |
console.assert(isEmpty(123.456) === false, `test isEmpty(123.456`) | |
console.assert(isEmpty('123.456') === false, `test isEmpty('123.456'`) | |
console.assert(isEmpty('pants') === false, `test isEmpty('pants'`) | |
/** | |
* Build a query string from a key/value object and optionally HTML encode it; implicitly replaces spaces with '+' | |
* @note Uses object decomposition for function arguments to make magic properties self-documenting in their call intances | |
* @param {object} config Your function argument should be an object contianing the following properties | |
* @param {object} data The key/value data object you want to conver to a query string | |
* @param {boolean} encoded (optional) Whether to HTML encode the generated queryString | |
* @param {boolean} allowNullProperties (optional) Allows for null properties to be written as key: undefined|null, but still writing the key into the query string rather than omitting it | |
*/ | |
function buildQueryString({data, encoded = false, allowNullProperties = false}) { | |
const queryString = Object.keys(data) | |
.map(key => { | |
let value = data[key] | |
if (!!allowNullProperties) { | |
return value ? `${key}=${value}` : key | |
} | |
return value ? `${key}=${value}` : null | |
}) | |
.filter(el => el !== null) | |
.join('&') | |
.replace(/\s/g, '+') | |
return !!encoded ? encodeURIComponent(queryString) : queryString | |
} | |
/** | |
* Sorts a collection of objects based on a pipe-delimited list of sorting parameter config strings | |
* @param {array} collection Collection of objects | |
* @param {string} sortKeys Pipe separated string of sortable properties within the collection model and it's sort priorities: | |
* @param {string} invertCollection invert collection sort | |
* | |
* @schema sortKeys: '(string)keyName:(string)keyType:(string)invert|...' | |
* @example sortKeys: 'isOldData:boolean|orderTotal:number:invert|productName:string' | |
* | |
* @return {array} The sorted collection | |
*/ | |
export function sortCollection(collection, sortKeys = '', invertCollection = false) { | |
if (!Array.isArray(collection)) { | |
let msg = 'collection must be of type Array' | |
console.error(msg, collection) | |
throw new Error(msg) | |
} | |
const TABLE_SORT_DIRECTION = invertCollection ? -1 : 1 | |
// split sortKeys string by pipes | |
sortKeys.split('|') | |
// for each sortKey | |
.map((el) => { | |
if (el.trim().length === 0) { | |
return 0 | |
} | |
// split the sortKey into it's key and dataType | |
let parts = el.split(':') | |
let keyName = parts[0].trim() | |
let keyType = (parts[1] || 'string').trim().toLowerCase() // presume the type is a string if not defined | |
let invertColumn = (parts[2] || '').trim().toLowerCase() === 'invert' ? -1 : 1 // a 3rd config prop should invert the sort | |
// console.debug('sortCollection', parts) | |
// sort collection by sortKey | |
collection.sort((a, b) => { | |
let aProp = a[keyName] | |
let bProp = b[keyName] | |
// manipulate comparator data based on datatype | |
switch(keyType) { | |
case 'string': | |
// ensure the string is actually a string and not null | |
aProp = aProp ? aProp + '' : '' | |
bProp = bProp ? bProp + '' : '' | |
aProp = aProp.toLowerCase().trim() | |
bProp = bProp.toLowerCase().trim() | |
break; | |
case 'number': | |
case 'boolean': | |
case 'date': | |
default: | |
break; | |
} | |
let sortDir = 0 | |
if (aProp < bProp) sortDir = -1 * TABLE_SORT_DIRECTION * invertColumn | |
if (aProp > bProp) sortDir = 1 * TABLE_SORT_DIRECTION * invertColumn | |
// console.debug('sortCollection :: sortDir', sortDir, aProp, bProp, TABLE_SORT_DIRECTION, invertColumn) | |
return sortDir | |
}) | |
}) | |
return collection | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use Spatie\YamlFrontMatter; | |
function readJSON(string $filepath) { | |
$json = file_get_contents($filepath); | |
$json = preg_replace('/\s\/\/[\s\S]+?\n/', '', $json); // remove comment strings | |
$json = preg_replace('/,\s+?([\}\]])/', '$1', $json); // replace trailing commas | |
return json_decode($json); | |
} | |
function writeJSON(string $filepath, $data, int $options = 0) : boolean { | |
$json = json_decode($data, $options); | |
return !!file_put_contents($filepath, $json); | |
} | |
/** | |
* Environment property lookup allowing for default answer | |
* @param string $propName Prop name to lookup | |
* @param any $default Optional default property to return | |
* @return any The value mapped in the environment or default property | |
*/ | |
function env(string $propName, $default = null) { | |
$prop = getenv($propName); | |
if ($prop === false && $default !== null) { | |
return $default; | |
} | |
if ($prop === 'true') { | |
return true; | |
} | |
if ($prop === 'false') { | |
return false; | |
} | |
return $prop; | |
} | |
/** | |
* Logging utility | |
* @param string $msg Message to log | |
* @param any $data Context data to be logged | |
* @return null | |
*/ | |
function logger(string $msg, $data = null) : void { | |
$timestamp = date('[ Y-m-d H:i:s ]'); | |
if ($data) { | |
$data = [ 'ctx' => $data ]; | |
$data = json_encode($data); | |
} else { | |
$data = ''; | |
} | |
$entry = trim("{$timestamp} {$msg} {$data}"); | |
file_put_contents("php://stdout", "\n{$entry}"); | |
} | |
/** | |
* Reads markdown file | |
* @param string $filepath Location of the file to be read | |
* @return object Parsed frontmatter data from file | |
*/ | |
function readMarkdown(string $filepath) : YamlFrontMatter\Document { | |
return YamlFrontMatter\YamlFrontMatter::parse(file_get_contents($filepath)); | |
} | |
/** | |
* Globs over a directory structure and returns a collection of matching filepaths | |
* @param string $folder Target directory to map over | |
* @param string $pattern String pattern to match | |
* @return array A list of filepath strings | |
*/ | |
function globFiles(string $folder, string $pattern) : array { | |
$dir = new RecursiveDirectoryIterator($folder); | |
$ite = new RecursiveIteratorIterator($dir); | |
$files = new RegexIterator($ite, $pattern, RegexIterator::GET_MATCH); | |
$fileList = []; | |
foreach($files as $file) { | |
$fileList = array_merge($fileList, $file); | |
} | |
return $fileList; | |
} | |
/** | |
* Method for slugifying a string allowing for a custom delimiter | |
* @param string $str Target string to slugify | |
* @param string $delimiter Replacement delimiter string/character | |
* @return string Slugified string | |
*/ | |
function slugify(string $str, string $delimiter = '-') : string { | |
return preg_replace('/\s/', $delimiter, strtolower(trim($str))); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use DI\Container; | |
use Psr\Http\Message\ResponseInterface as Response; | |
use Psr\Http\Message\ServerRequestInterface as Request; | |
use Slim\Factory\AppFactory; | |
use Slim\Views\Twig; | |
use Slim\Views\TwigMiddleware; | |
use Twig\TwigFilter; | |
use Twig\TwigFunction; | |
// Define app resource path(s) | |
$ROOT_DIR = getcwd(); | |
define('ROOT_DIR', $ROOT_DIR); | |
define('ASSETS_DIR', "{$ROOT_DIR}/public"); | |
define('CACHE_DIR', "{$ROOT_DIR}/cache"); | |
define('CONFIGS_DIR', "{$ROOT_DIR}/config"); | |
define('CONTENT_DIR', "{$ROOT_DIR}/content"); | |
define('LOGS_DIR', "{$ROOT_DIR}/logs"); | |
define('VIEWS_CACHE', "{$ROOT_DIR}/cache/views"); | |
define('VIEWS_DIR', "{$ROOT_DIR}/src/views"); | |
// Load project resources | |
require './vendor/autoload.php'; | |
require './src/helpers.php'; | |
// Initialize local environment | |
$dotenv = Dotenv\Dotenv::createImmutable(ROOT_DIR); | |
$dotenv->load(); | |
$dotenv->required([ | |
'APP_ENV', | |
// 'APP_HOSTNAME', | |
'APP_TIMEZONE', | |
// 'APP_TITLE', | |
// 'DB_DATABASE', | |
// 'DB_HOSTNAME', | |
// 'JWT_ALGORITHM', | |
// 'JWT_SECRET', | |
// 'JWT_SECRET', | |
]); | |
// Determine our app environment | |
$IS_DEV = env('ENV') !== 'build'; | |
define('IS_DEV', $IS_DEV); | |
define('IS_PROD', !$IS_DEV); | |
// Set application default timezone | |
date_default_timezone_set(env('APP_TIMEZONE')); | |
// Create Container | |
$container = new Container(); | |
AppFactory::setContainer($container); | |
// Set view in Container | |
$container->set('view', function() { | |
$config = [ | |
'cache' => IS_PROD ? VIEWS_CACHE : false, | |
]; | |
$twig = Twig::create(VIEWS_DIR, $config); | |
$env = $twig->getEnvironment(); | |
// add asset() function | |
$env->addFunction(new TwigFunction('asset', function($filepath) { | |
$prefix = env('BASE_URL'); | |
if (is_file(ASSETS_DIR . $filepath)) { | |
return "{$prefix}{$filepath}"; | |
} | |
return "bad filepath: {$filepath}"; | |
})); | |
$env->addFunction(new TwigFunction('url', function($path) { | |
$url = env('BASE_URL'); | |
return $url . $path; | |
})); | |
$env->addFilter(new TwigFilter('slugify', 'slugify')); | |
return $twig; | |
}); | |
// Set view model in Container | |
$container->set('viewModel', function() { | |
$model = readJSON(CONFIGS_DIR . '/site.json'); | |
$model->filemap = readJSON(CACHE_DIR . '/pages.json'); | |
$model->baseurl = env('BASE_URL'); | |
$model->today = date('Y-m-d'); | |
$model = json_encode($model); | |
$model = json_decode($model, true); | |
return $model; | |
}); | |
// Create App | |
$app = AppFactory::create(); | |
// Add error middleware | |
$app->addErrorMiddleware(true, true, true); | |
// Add Twig-View Middleware | |
$app->add(TwigMiddleware::createFromContainer($app)); | |
// // Editor view | |
// $app->get('/editor', function(Request $req, Response $res) { | |
// $template = 'hello.twig'; | |
// $model = $this->get('viewModel'); | |
// $model['name'] = $args['name']; | |
// $model['pageTitle'] = 'Page Title'; | |
// return $this->get('view')->render($res, $template, $model); | |
// })->setName('editor-ui'); | |
// Blog content pages | |
$app->get('{filepath:.+}', function(Request $req, Response $res, $filepath) { | |
$url = parse_url($_SERVER['REQUEST_URI']); | |
$model = $this->get('viewModel'); | |
$filepath = $model['filemap'][$url['path']] ?? null; | |
if (!$filepath) { | |
echo "{$filepath} does not exist..."; | |
return $res; | |
} | |
$content = readMarkdown(ROOT_DIR . $filepath); | |
$model['draft'] = $content->draft; | |
$model['pageTitle'] = $content->title; | |
$model['published'] = $content->published ?? date(env('DATE_FORMAT')); | |
$model['tags'] = $content->tags ?? []; | |
$model['updated'] = $content->updated ?? null; | |
$model['description'] = $content->description ?? null; | |
$model['robots'] = $content->robots ?? 'all'; | |
$model['license'] = $content->license ?? $model['license']; | |
// $model['PROP'] = $content->PROP ?? 'PROP'; | |
$model['content'] = $content->body(); | |
$template = $content->template ?? 'default.twig'; | |
return $this->get('view')->render($res, $template, $model); | |
})->setName('page-render'); | |
// Run the app | |
$app->run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment