Skip to content

Instantly share code, notes, and snippets.

@bmcminn
Last active April 21, 2022 18:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmcminn/61b4d8454fe36da11e428b011612947b to your computer and use it in GitHub Desktop.
Save bmcminn/61b4d8454fe36da11e428b011612947b to your computer and use it in GitHub Desktop.
// 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
}
<?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)));
}
<?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