code:
			<?php _e( 'Order Num:', 'Shopp' ); ?>
			<?php shopp('purchase','id'); ?>

			<?php _e( 'Order Date:', 'Shopp' ); ?>
			<?php shopp( 'purchase.date' ); ?>

			<?php _e( 'Billed To:', 'Shopp' ); ?>
			<?php shopp( 'purchase.card' ); ?> (<?php shopp( 'purchase.cardtype' ); ?>)

			<?php _e( 'Transaction:', 'Shopp' ); ?>
			<?php shopp( 'purchase.transactionid' ); ?> (<?php shopp( 'purchase.payment' ); ?>)

				<fieldset class="billing">
					<legend><?php _e( 'Billed to', 'Shopp' ); ?></legend>
					<address>
						<?php shopp( 'purchase.firstname' ); ?> <?php shopp( 'purchase.lastname' ); ?>

						<?php shopp( 'purchase.company' ); ?>

						<?php shopp( 'purchase.address' ); ?>

						<?php shopp( 'purchase.xaddress' ); ?>
						<?php shopp( 'purchase.city' ); ?>, <?php shopp( 'purchase.state' ); ?> <?php shopp( 'purchase.postcode' ); ?>

						<?php shopp( 'purchase.country' ); ?>
					</address>
				</fieldset>

			<?php if ( shopp( 'purchase.hasfreight' ) ) : ?>

					<fieldset class="shipping">
						<legend><?php _e( 'Ship to', 'Shopp' ); ?></legend>
						<address>
							<?php shopp( 'purchase.shipname' ); ?>

							<?php shopp( 'purchase.shipaddress' ); ?>

							<?php shopp( 'purchase.shipxaddress' ); ?>
							<?php shopp( 'purchase.shipcity' ); ?>, <?php shopp( 'purchase.shipstate' ); ?> <?php shopp( 'purchase.shippostcode' ); ?>

							<?php shopp( 'purchase.shipcountry' ); ?>
						</address>

<?php _e( 'Shipping:', 'Shopp' ); ?> <?php shopp( 'purchase.shipmethod' ); ?>

					</fieldset>

			<?php endif; ?>

	<?php if ( shopp( 'purchase.hasitems' ) ) : ?>

					<?php _e( 'Items Ordered', 'Shopp' ); ?>
					<?php _e( 'Quantity', 'Shopp' ); ?>
					<?php _e( 'Item Price', 'Shopp' ); ?>
					<?php _e( 'Item Total', 'Shopp' ); ?>

			<?php while ( shopp( 'purchase.items' ) ) : ?>

						<?php shopp( 'purchase.item-name' ); ?><?php shopp( 'purchase.item-options', 'before= – ' ); ?>

						<?php shopp( 'purchase.item-sku' ); ?>

						<?php shopp( 'purchase.item-download' ); ?>
						<?php shopp( 'purchase.item-addons-list' ); ?>
						<?php shopp( 'purchase.item-inputs-list' ); ?>

					<?php shopp( 'purchase.item-quantity' ); ?>
					<?php shopp( 'purchase.item-unitprice' ); ?>
					<?php shopp( 'purchase.item-total' ); ?>

			<?php endwhile; ?>

				<?php _e( 'Subtotal', 'Shopp' ); ?>
				<?php shopp( 'purchase.subtotal' ); ?>

			<?php if ( shopp( 'purchase.hasdiscount' ) ) : ?>

					<?php _e( 'Discount', 'Shopp' ); ?>
					-<?php shopp( 'purchase.discount' ); ?>

			<?php endif; ?>

			<?php if ( shopp( 'purchase.hasfreight' ) ) : ?>

					<?php _e( 'Shipping', 'Shopp' ); ?>
					<?php shopp( 'purchase.shipping' ); ?>

			<?php endif; ?>

			<?php if ( shopp( 'purchase.hastax' ) ) : ?>

					<?php _e( 'Tax', 'Shopp' ); ?>
					<?php shopp( 'purchase.tax' ); ?>

			<?php endif; ?>

				<?php _e( 'Total', 'Shopp' ); ?>
				<?php shopp( 'purchase.total' ); ?>

		<?php if ( shopp( 'purchase.has-data' ) ) : ?>

				<?php while ( shopp( 'purchase.orderdata' ) ) : ?>
					<?php if ( shopp( 'purchase.get-data' ) == '' ) continue; ?>

<?php shopp( 'purchase.data', 'name' ); ?>: <?php shopp( 'purchase.data' ); ?>

				<?php endwhile; ?>

		<?php endif; ?>

	<?php else : ?>

<?php _e( 'There were no items found for this purchase.', 'Shopp' ); ?>

	<?php endif; ?>


code:
/**
 * shopp.css
 * Generic layout & styles for Shopp pages
 *
 * @author Jonathan Davis
 * @package shopp
 **/

/** Generic Shopp Styles **/

#shopp .submit { text-align: right; }
#shopp form { text-align: left; }
#shopp form.product ul.options { list-style: none; }
#shopp form ul { line-height: 1; }
#shopp form li > label { font-weight: bold; padding: 20px 0 10px; }
#shopp form ul li span label,
#shopp form div label { font-size: 0.8125em; font-weight: normal; padding-top: 0.6em; }
#shopp .right { float: right; }

/** Catalog/Category **/

.categories ul li a.current { font-weight: bold; }
#shopp .category { position: relative; }
#shopp .category-description { padding-bottom: 14px; }
#shopp .views,
#shopp .views li { display: inline; }
#shopp .orderby { float: right; }

#shopp .category .subcategories { padding: 10px 0; margin-bottom: 10px; }
#shopp .category .subcategories ul { padding: 10px 0 0 20px; margin-bottom: 10px; }
#shopp .category .subcategories li { display: inline-block; margin-right: 20px; vertical-align: top; }
#shopp form.category-list-menu .subcategories { padding: auto; }
#shopp .slideshow,
#shopp .carousel { margin: 20px 0; overflow: hidden; }

/* Each product in a category has a containing frame */
#shopp ul.products li.product div.frame { padding: 10px; }

#shopp ul.products li .name,
#shopp ul.products li .summary,
#shopp ul.products li .savings,
#shopp ul.products li .price { margin-top: 0; margin-bottom: 0; }
#shopp ul.products li .details { float: none; }

/* Hide the product summary in the grid view */
#shopp ul.products li .summary { display: none; }
#shopp ul.products li .listview { display: none; }

#shopp .price { font-weight: bold; line-height: 1.5; margin: 0; }
#shopp .original { text-decoration: line-through; }
#shopp .sale { font-size: 150%; }

/* Faceted menu cancel filter link styles */
.filters a.cancel:before { content: "["; }
.filters a.cancel { font-weight: bold; }
.filters a.cancel:after { content: "]"; }
.filters a.cancel:hover { text-decoration: none; }

/** Product List View **/
#shopp.list ul.products li { float: none; width: 100%; overflow: hidden; padding-bottom: 10px;}
#shopp.list ul.products li.product {  border-bottom: 1px solid #ddd; }
#shopp.list ul.products li div.frame { width: 100%; text-align: left; }
#shopp.list ul.products li img { float: left; margin-right: 10px; }
#shopp.list ul.products li .details { float: left; width: 65%; }
#shopp.list ul.products li .listview { display: block; }

#shopp ul.breadcrumb { margin: 10px 0; display: inline; }
#shopp ul.breadcrumb li { background: none; padding: 0; display: inline; margin-left: 0; text-indent: 0; }
#shopp ul.breadcrumb li span { font-weight: bold; }
#shopp .category-list-menu { display: inline; }

#shopp ul.categories { float: left; margin-right: 20px; }

/** Category Pagination **/
#shopp ul.paging { /* placeholder for your styles - inherited defaults: clear: none; display: inline-block; */ }
#shopp ul.paging li { /* placeholder for your styles - inherited defaults: display: inline-block; margin-left: 5px; */ }
#shopp ul.paging li a { /* page links default styling */ }
#shopp ul.paging li a:hover { /* page links hover styling */ }
#shopp ul.paging li.active { /* currrent page style */ }
#shopp ul.paging li.disabled { /* style for disabled previous/next links */ }
#shopp ul.paging li.previous a,
#shopp ul.paging li.next a { /* previous/next link style */ }

/** Product Pages **/
#shopp .column { float: left; }
#shopp .column.first { width: 65%; }
#shopp .column.second { float: right; width: 35%; }

#shopp .original { text-decoration: line-through; }
#shopp form.product ul,
#shopp form.product ul li,
#shopp form.product ul li label { clear: none; }
#shopp form.product ul { width: auto; }
#shopp ul.variations label { display: block; }
#shopp dl { overflow: hidden; }
#shopp dt { clear:left; float:left; text-align: right; width:50%; margin-right: 1em; }
#shopp dd { clear:right; width:50%; padding-left: 50%; }

#shopp .gallery { margin: 5px 10px 5px 0; }
#shopp .gallery ul.thumbnails { margin: 10px 0 10px 0; }
#shopp .gallery ul.thumbnails li { margin: 0 5px 0 0; }

/** Category List Widget **/
ul.shopp_categories { /* category list container */ }
ul.shopp_categories li { /* category listing containers */ }
ul.shopp_categories li a { /* category link default styles */ }
ul.shopp_categories li a:hover { /* category link hover styles */ }
ul.shopp_categories li span { /* categpry product count styles */ }
ul.shopp_categories li.current { /* currently viewed category container */ }
ul.shopp_categories li.current a { /* currently viewed category link style */ }
ul.shopp_categories li.current a:hover { /* currently viewed category link hover style */ }
ul.shopp_categories li ul.children { /* sub-category list container */ }
ul.shopp_categories li ul.children li { /* sub-category listing container */ }
ul.shopp_categories li ul.active { /* active sub-category container */ }

/* Collapsed sub-category menus example
ul.shopp_categories li ul.children { display: none; }
ul.shopp_categories li ul.active { display: block; }
*/

/** Side Products Widget **/
.sideproduct { margin-top: 20px; }
.sideproduct h3 { margin-top: 0; }
.sideproduct .original.price { text-decoration: line-through; }

/** Shopping Cart **/
#shopp #cart { width: 100%; }
#shopp #cart table { padding: 10px 0; width: 100%; }
#shopp #cart th { text-align: left; font-weight: bold; }
#shopp #cart th, #shopp #cart td { padding: 3px; }
#shopp #cart .item { width: 50%; }
#shopp #cart .money,
#shopp #cart .totals th,
#shopp #cart .buttons td { text-align: right; white-space: nowrap; }
#shopp #cart .totals.total th,
#shopp #cart .totals.total td { font-size: 130%; }
#shopp #cart .remove { font-size: 9px; }

#shopp #cart address { font-style: normal; }
#shopp #cart big { display: block; margin: 10px 0; }
#shopp #cart big a.right { float: right; }

/** Shopping Cart Widget **/
#shopp-cart.widget p.status { margin-bottom: 10px; }

#shopp ul.account li { display: inline-block; width: 200px; padding-top: 18px; }

/** Checkout **/
#shopp #shipping-methods { padding-left: 30px; }
#shopp #shipping-methods li { margin-top: 5px; }
#shopp #shipping-methods li label { padding: 0 0 10px; }
#shopp #shipping-methods input { margin-left: -16px; }

#shopp .shopp-errors { color: #990000; }
#shopp input.error { background: #ffdddd; }
#shopp label.error { color: #990000; font-weight: bold; }

.clickwrap-terms { overflow: auto; height: 200px; border: 1px solid #333; padding: 0.5em; margin-bottom: 1em; }

/** Order Receipt **/
#shopp #receipt { position: relative; margin: 20px 0; }
#shopp #receipt address { font-style: normal; }
#shopp #receipt fieldset { padding: 10px; border: 1px solid #dadada; margin: 20px 0 30px; }
#shopp #receipt fieldset legend { display: block; }
#shopp #receipt table { overflow: hidden; }
#shopp #receipt table.transaction th { text-align: left; }
#shopp #receipt .labels { width: 100%; }
#shopp #receipt table.labels td { vertical-align: top; }

#shopp #receipt .order { width: 100%; overflow: hidden; border: none; }
#shopp #receipt td { border: none; }
#shopp #receipt th { font-weight: bold; text-align: left; border: none; }
#shopp #receipt th,
#shopp #receipt td { padding: 3px 0; }
#shopp #receipt .order .item { width: 50%; }
#shopp #receipt .order td.qty { text-align: center; }
#shopp #receipt .order .money,
#shopp #receipt .order .total,
#shopp #receipt .order .buttons td { text-align: right; }
#shopp #receipt .order .totals.total th,
#shopp #receipt .order .totals.total td { font-size: 130%; }

repo_name: ITSociaal/hg

license: mit


code:
	<?php if ( shopp( 'cart.hasitems' ) ) : ?>

			<?php shopp( 'cart.totalitems' ); ?> <?php _e( 'Items', 'Shopp' ); ?>

			<?php shopp( 'cart.total' ); ?> <?php _e( 'Total', 'Shopp' ); ?>

<a href="<?php shopp( 'cart.url' ); ?>"><?php _e( 'Edit shopping cart', 'Shopp' ); ?>

<a href="<?php shopp( 'checkout.url' ); ?>"><?php _e( 'Proceed to Checkout', 'Shopp' ); ?>

	<?php else : ?>

<?php _e( 'Your cart is empty.', 'Shopp' ); ?>

	<?php endif; ?>


code:
<?php if ( shopp( 'product.found' ) ) : ?>

		<a href="<?php shopp( 'product.url' ); ?>"><?php shopp( 'product.coverimage', 'setting=thumbnails' ); ?>

<a href="<?php shopp( 'product.url' ); ?>"><?php shopp( 'product.name' ); ?>

		<?php if ( shopp( 'product.onsale' ) ) : ?>

<?php shopp( 'product.price' ); ?>

<?php shopp( 'product.saleprice' ); ?>

		<?php else : ?>

<?php shopp( 'product.price' ); ?>

		<?php endif; ?>

<?php endif; ?>


code:
<?php if ( shopp( 'cart.hasitems' ) ) : ?>

				<?php _e( 'Cart Items', 'Shopp' ); ?>
				<?php _e( 'Quantity', 'Shopp' ); ?>
				<?php _e( 'Item Price', 'Shopp' ); ?>
				<?php _e( 'Item Total', 'Shopp' ); ?>

			<?php while ( shopp( 'cart.items' ) ) : ?>

						<a href="<?php shopp( 'cartitem.url' ); ?>"><?php shopp( 'cartitem.name' ); ?>
						<?php shopp( 'cartitem.options', 'show=selected&before= (&after=)' ); ?>
						<?php shopp( 'cartitem.inputs-list' ); ?>
						<?php shopp( 'cartitem.addons-list' ); ?>

					<?php shopp( 'cartitem.quantity' ); ?>
					<?php shopp( 'cartitem.unitprice' ); ?>
					<?php shopp( 'cartitem.total' ); ?>

			<?php endwhile; ?>

			<?php while ( shopp( 'cart.promos' ) ) : ?>

					<?php shopp( 'cart.promo-name' ); ?><?php shopp( 'cart.promo-discount', array( 'before' => ' — ' ) ); ?>

			<?php endwhile; ?>

					<?php if ( ( shopp( 'cart.has-shipping-methods' ) ) ) : ?>
						<?php _e( 'Select a shipping method:', 'Shopp' ); ?>
						<form action="<?php shopp( 'shipping.url' ); ?>" method="post">

								<?php while ( shopp( 'shipping.methods' ) ) : ?>

											<?php shopp( 'shipping.method-selector' ); ?>
											<?php shopp( 'shipping.method-name' ); ?> —
											<?php shopp( 'shipping.method-cost' ); ?>

											<?php shopp( 'shipping.method-delivery' ); ?>

								<?php endwhile; ?>

							<?php shopp( 'shipping.update-button' ); ?>
						</form>
					<?php endif; ?>

				<?php _e( 'Subtotal', 'Shopp' ); ?>
				<?php shopp( 'cart.subtotal' ); ?>

			<?php if ( shopp( 'cart.hasdiscount' ) ) : ?>

					<?php _e( 'Discount', 'Shopp' ); ?>
					-<?php shopp( 'cart.discount' ); ?>

			<?php endif; ?>

			<?php if ( shopp( 'cart.needs-shipped' ) ) : ?>

					<?php shopp( 'cart.shipping', 'label=' . __( 'Shipping', 'Shopp' ) ); ?>
					<?php shopp( 'cart.shipping' ); ?>

			<?php endif; ?>

				<?php shopp( 'cart.tax', 'label=' . __( 'Taxes', 'Shopp' ) ); ?>
				<?php shopp('cart','tax' ); ?>

				<?php _e( 'Total', 'Shopp' ); ?>
				<?php shopp( 'cart.total' ); ?>

		<?php if ( shopp( 'checkout.hasdata' ) ) : ?>

				<?php while ( shopp( 'checkout.orderdata' ) ) : ?>

<?php shopp( 'checkout.data', 'name' ); ?>: <?php shopp( 'checkout.data' ); ?>

				<?php endwhile; ?>

		<?php endif; ?>

<?php else : ?>

<?php _e( 'There are currently no items in your shopping cart.', 'Shopp' ); ?>

<a href="<?php shopp( 'storefront.url' ); ?>">« <?php _e( 'Continue Shopping', 'Shopp' ); ?>

<?php endif; ?>


code:
<?php _e( 'Thank you for your order!', 'Shopp' ); ?>

<?php if ( shopp( 'checkout.completed' ) ) : ?>

	<?php if ( shopp( 'purchase.notpaid' ) ) : ?>

<?php _e( 'Your order has been received but the payment has not yet completed processing.', 'Shopp' ); ?>

		<?php if ( shopp( 'checkout.get-offline-instructions' ) ) : ?>
			<?php shopp( 'checkout.offline-instructions' ); ?>
		<?php endif; ?>

		<?php if ( shopp( 'purchase.hasdownloads' ) ) : ?>

<?php _e( 'The download links on your order receipt will not work until the payment is received.', 'Shopp' ); ?>

		<?php endif; ?>

		<?php if ( shopp( 'purchase.hasfreight' ) ) : ?>

<?php _e( 'Your items will not ship out until the payment is received.', 'Shopp' ); ?>

		<?php endif; ?>

	<?php endif; ?>

	<?php shopp( 'checkout.receipt' ); ?>

	<?php if ( shopp( 'customer.wpuser-created' ) ) : ?>

<?php _e( 'An email was sent with account login information to the email address provided for your order.', 'Shopp' ); ?>  <a href="<?php shopp( 'customer.url' ); ?>"><?php _e( 'Login to your account', 'Shopp' ); ?> <?php _e( 'to access your orders, change your password and manage your checkout information.', 'Shopp' ); ?>

	<?php endif; ?>

<?php else : ?>

<?php _e( 'Your order is still in progress. Payment for your order has not yet been received from the payment processor. You will receive an email notification when your payment has been verified and the order has been completed.', 'Shopp' ); ?>

<?php endif; ?>


code:
<?php
namespace ITSociaal\Themes\Houtgrutter;

class StringRenderer implements RendererInterface
{
	/** @var string */
	protected $string = "";

	/**
	 * @return string
	 */
	public function getString()
	{
		return $this->string;
	}

	/**
	 * @param string $string
	 *
	 * @return self
	 */
	public function setString($string)
	{
		$this->string = $string;

		return $this;
	}

	/** {@inheritDoc} */
	public function render()
	{
		return $this->getString();
	}
}

/* vi:set ts=4 sw=4 noet: */


code:
<?php
namespace ITSociaal\Themes\Houtgrutter;

abstract class StringRendererFactory implements RendererFactoryInterface
{
	/**
	 * Produces RendererInterface for the given template name
	 *
	 * Based on template name alone, for testing purposes
	 *
	 * @param string                             $name
	 * @param \ITSociaal\Themes\Houtgrutter\Util $util
	 *
	 * @return \ITSociaal\Themes\Houtgrutter\RendererInterface
	 */
	public static function create($name, Util $util)
	{
		$name = htmlspecialchars(
			$name,
			ENT_NOQUOTES|ENT_SUBSTITUTE|ENT_DISALLOWED|ENT_HTML5
		);

		return $util
			->createObject(StringRenderer::class)
			->setString("<h1>$name</h1>");
	}
}

/* vi:set ts=4 sw=4 noet: */


code:
<?php
namespace ITSociaal\Themes\Houtgrutter;

class TemplateController
{
	/** @var \ITSociaal\Themes\Houtgrutter\RenderController */
	protected $rc;

	/** @var \ITSociaal\Themes\Houtgrutter\Util */
	protected $util;

	/** @var string */
	protected $name;

	/** @var callable[] */
	protected $rendererFactories = [];

	/**
	 * Returns $obj only if valid RendererInterface instance
	 *
	 * @return \ITSociaal\Themes\Houtgrutter\RendererInterface|null
	 */
	protected function filterValidRenderer($obj)
	{
		return is_a($obj, RendererInterface::class)
			? $obj
			: null;
	}

	/**
	 * Returns $obj only if valid PostProcessorInterface instance
	 *
	 * @return \ITSociaal\Themes\Houtgrutter\PostProcessorInterface|null
	 */
	protected function filterValidPostProcessor($obj)
	{
		return is_a($obj, PostProcessorInterface::class)
			? $obj
			: null;
	}

	/**
	 * @param callable $factory
	 *
	 * @return self
	 */
	protected function addRendererFactory(callable $factory)
	{
		$this->rendererFactories[] = $factory;

		return $this;
	}

	/**
	 * @return callable[]
	 */
	public function getRendererFactories()
	{
		return $this->rendererFactories;
	}

	/**
	 * Fills the RenderController with valid objects
	 *
	 * @return self
	 */
	public function findRenderer()
	{
		$util = $this->getUtil();
		$rc = $this->getRenderController();

		foreach (
			$util->generateResultPerMethod(
				$this->getRendererFactories(),
				$this->getName(),
				$util
			) as $result
		) {
			$renderer = $this->filterValidRenderer($result);
			$postproc = $this->filterValidPostProcessor($result);

			!is_object($renderer) or $rc->addRenderer($renderer);
			!is_object($postproc) or $rc->addPostProcessor($postproc);
		}

		return $this;
	}

	/**
	 * Primes the controller with renderer factories
	 *
	 * Should be called before findRenderer()
	 *
	 * @return self
	 */
	public function addDefaultRendererFactories()
	{
		$this->addRendererFactory([GenericRendererFactory::class, "create"]);
		$this->addRendererFactory([WpRendererFactory::class, "create"]);
		$this->addRendererFactory([StringRendererFactory::class, "create"]);

		return $this;
	}

	/**
	 * @param \ITSociaal\Themes\Houtgrutter\Util $util
	 */
	public function __construct(Util $util, RenderController $rc)
	{
		$this->util = $util;
		$this->rc = $rc;
	}

	/**
	 * Does all the things
	 *
	 * @param string                                           $template
	 * @param \ITSociaal\Themes\Houtgrutter\Util               $util
	 * @param \ITSociaal\Themes\Houtgrutter\RenderController   $rc
	 * @param \ITSociaal\Themes\Houtgrutter\TemplateController $self
	 */
	public static function main(
		$template,                        // Should be __FILE__ in caller
		Util $util = null,
		RenderController $rc = null,
		TemplateController $self = null
	) {
		$util = $util ?: new Util;
		$rc   = $rc   ?: new RenderController($util);
		$self = $self ?: new static($util, $rc);

		print($self
			->addDefaultRendererFactories()
			->setName($self->getUtil()->deriveTemplateNameFromPath($template))
			->findRenderer()
			->getRenderController()
			->performRendering()
			->performPostProcessing()
			->getResult());
	}

	/**
	 * @return string
	 * @throws \ITSociaal\Themes\Houtgrutter\Exception\RuntimeException
	 */
	public function getName()
	{
		$ret = $this->name;

		if (is_null($ret)) {
			throw new Exception\RuntimeException(
				"name has not been set"
			);
		}

		return $ret;
	}

	/**
	 * @param string $name
	 *
	 * @return self
	 * @throws \ITSociaal\Themes\Houtgrutter\Exception\InvalidArgumentException
	 */
	public function setName($name)
	{
		$name = (string)$name;

		if (empty($name)) {
			throw new Exception\InvalidArgumentException(
				"\$name should not be empty"
			);
		}

		$this->name = $name;

		return $this;
	}

	/**
	 * @return \ITSociaal\Themes\Houtgrutter\Util
	 */
	protected function getUtil()
	{
		return $this->util;
	}

	/**
	 * @return \ITSociaal\Themes\Houtgrutter\RenderController
	 */
	protected function getRenderController()
	{
		return $this->rc;
	}
}

/* vi:set ts=4 sw=4 noet: */


code:
<?php
namespace ITSociaal\Themes\Houtgrutter;

class TemplateProcessor
{
	/** @var object */
	protected $context;

	/**
	 * @param string $template
	 *
	 * @return array[]
	 */
	protected function findMatches($template)
	{
		$matches = [];

		preg_match_all(
			"/%%(\\w+)%%/",
			$template,
			$matches,
			PREG_OFFSET_CAPTURE|PREG_SET_ORDER
		);

		return array_map(function($match) {
			return [$match[0][1], strlen($match[0][0]), $match[1][0]];
		}, $matches);
	}

	/**
	 * @param string  $template
	 * @param array[] $matches
	 *
	 * @return string
	 */
	protected function processMatches($template, $matches)
	{
		return array_reduce(
			array_reverse($matches),
			[$this, "processSingleMatch"],
			$template
		);
	}

	/**
	 * @param string $carry
	 * @param array  $item
	 *
	 * @return string
	 */
	protected function processSingleMatch($carry, $item)
	{
		$replacement = $this->getValue($item[2]);

		return
			substr($carry, 0, $item[0]) .
			$replacement .
			substr($carry, $item[0] + $item[1]);
	}

	/**
	 * @param string $key
	 *
	 * @return string
	 */
	protected function getValue($key)
	{
		$context = $this->getContext();

		try {
			$ret = (string)call_user_func([$context, "get" . $key]);
		}
		catch (\Exception $ex) {
			/* Not sure what should be done in this case,
			 * safest thing probably is to pretend as if
			 * nothing happened.
			 */
			$ret = "%%$key%%";
		}

		return $ret;
	}

	/**
	 * Set context object to get tag values from
	 *
	 * @param object $context
	 *
	 * @return self
	 * @throws \ITSociaal\Themes\Houtgrutter\Exception\InvalidArgumentException
	 */
	public function setContext($context)
	{
		if (!is_object($context)) {
			throw new Exception\InvalidArgumentException(
				"\$context must be a valid object"
			);
		}

		$this->context = $context;

		return $this;
	}

	/**
	 * @return object
	 * @throws \ITSociaal\Themes\Houtgrutter\Exception\RuntimeException
	 */
	public function getContext()
	{
		$context = $this->context;

		if (null === $context) {
			throw new Exception\RuntimeException(
				"Context has not been set"
			);
		}

		return $context;
	}

	/**
	 * Parse $template and replace %%tags%%
	 *
	 * @param string $template
	 *
	 * @return string
	 */
	public function process($template)
	{
		return $this->processMatches(
			$template,
			$this->findMatches($template)
		);
	}
}

/* vi:set ts=4 sw=4 noet: */


code:
<?php
namespace ITSociaal\Themes\Houtgrutter;

/* These methods should be static, but PHPUnit doesn't allow it:
 *
 *   "Please note that final, private and static methods cannot
 *    be stubbed or mocked.  They are ignored by PHPUnit's test
 *    double functionality and retain their original behavior."
 */
class Util
{
	/**
	 * Invokes the new operator for you :)
	 *
	 * @param string $class
	 *
	 * @return object
	 */
	public function createObject($class, $arg = null)
	{
		return new $class($arg);
	}

	/**
	 * Not orthogonal to deriveTemplatePathFromName
	 *
	 * @param string $path
	 *
	 * @return string Relative path to template
	 */
	public function deriveTemplateNameFromPath($path)
	{	// This could use some refactoring for robustness

		if (($path[0] === "/") || (substr($path, -4) === ".php")) {
			$comp = explode(
				realpath(theme_root_path()),
				realpath($path)
			);

			if (($comp[0] === "") && (isset($comp[1]))) {
				$ret = strtolower($comp[1]);
			}
		}
		else {
			$ret = $path;
		}

		$ret = ltrim(strtr($ret, "\\", "/"), "/");

		if (substr($ret, -4) === ".php") {
			$ret = substr($ret, 0, -4);
		}

		return $ret;
	}

	/**
	 * Derives a class name from a relative path
	 *
	 * @param string $name Template name
	 *
	 * @return string Possible name of a class
	 */
	public function deriveClassNameFromTemplateName($name)
	{
		$pieces = explode("/", strtolower($name));

		return __NAMESPACE__ . "\\" .
			str_replace(
				"-",
				"",
				implode(
					"\\",
					array_map(
						"ucwords",
						$pieces,
						array_fill(0, count($pieces), "-\\")
					)
				)
			);
	}

	/**
	 * Not orthogonal to deriveTemplateNameFromPath
	 *
	 * @param string $name      Template name
	 * @param string $extension Extension, e.g. ".md"
	 *
	 * @return string Possible path to a template
	 */
	public function deriveTemplatePathFromName($name, $extension = ".php")
	{
		$parts = explode("/", $name);
		$last = "templates/" . strtolower(array_pop($parts));
		$parts = array_map(function($part) {
			return str_replace("-", "", ucwords($part, "-"));
		}, $parts);
		$parts[] = $last;

		return SRCDIR . "/" . implode("/", $parts) . $extension;
	}

	/**
	 * array_map as a generator, with flipped arguments
	 *
	 * @param array    $args   Arguments passed to $method one by one
	 * @param callable $method Mapping function
	 *
	 * @return \Iterator yields return values of $method
	 */
	public function generateResultPerArgument(array $args, callable $method)
	{
		foreach ($args as $arg) {
			yield call_user_func($method, $arg);
		}
	}

	/**
	 * Same as generateResultPerArgument, but sideways
	 *
	 * @param array $methods Set of callables to iterate over
	 * @param mixed ...$args Optional arguments passed to $methods each time
	 *
	 * @return \Iterator yields return value of each callable
	 */
	public function generateResultPerMethod(array $methods, ...$args)
	{
		foreach ($methods as $method) {
			yield call_user_func_array($method, $args);
		}
	}
}

/* vi:set ts=4 sw=4 noet: */


code:
<?php
namespace ITSociaal\Themes\Houtgrutter;

abstract class WordPressBase
{
	private $_wpi;

	protected function getWP()
	{
		return $this->_wpi = $this->_wpi ?: new GlobalWordPress;
	}
}

/* vi:set ts=4 sw=4 noet: */


code:
<?php
namespace ITSociaal\Themes\Houtgrutter;

interface WordPressInterface
{
	public function avia_append_search_nav();

	public function avia_get_option();

	public function get_bloginfo();

	public function get_the_post_thumbnail();

	public function has_post_thumbnail();

	public function load_template();

	public function shopp();

	public function wp_nav_menu();
}

/* vi:set ts=4 sw=4 noet: */


code:
<?php
namespace ITSociaal\Themes\Houtgrutter;

class WpRenderer extends WordPressBase implements RendererInterface
{
	/** @var string */
	protected $file;

	/**
	 * Set name of WP template file
	 *
	 * @param string $file
	 *
	 * @return self
	 * @throws \ITSociaal\Themes\Houtgrutter\Exception\InvalidArgumentException
	 */
	public function setFile($file)
	{
		$file = (string)$file;

		if (empty($file)) {
			throw new Exception\InvalidArgumentException(
				"\$file must not be empty"
			);
		}

		$this->file = $file;

		return $this;
	}

	/**
	 * Get name of WP template file
	 *
	 * @return string|null
	 * @throws \ITSociaal\Themes\Houtgrutter\Exception\RuntimeException
	 */
	public function getFile()
	{
		$file = $this->file;

		if (is_null($file)) {
			throw new Exception\RuntimeException(
				"file has not been set"
			);
		}

		return $file;
	}

	/**
	 * Render the WP template
	 *
	 * @return string
	 */
	public function render()
	{
		$file = $this->getFile();
		$wp   = $this->getWP();

		try {
			ob_start();
			$wp->load_template($file, false);
			return ob_get_clean();
		} catch (\Exception $ex) {
			ob_end_clean();
			throw $ex;
		}
	}
}

/* vi:set ts=4 sw=4 noet: */


code:
<?php
namespace ITSociaal\Themes\Houtgrutter;

abstract class WpRendererFactory implements RendererFactoryInterface
{
	/**
	 * Produces RendererInterface for the given template name
	 *
	 * Based on wordpress template residing in src/.../templates/
	 *
	 * @param string                             $name
	 * @param \ITSociaal\Themes\Houtgrutter\Util $util
	 *
	 * @return \ITSociaal\Themes\Houtgrutter\RendererInterface|null
	 */
	public static function create($name, Util $util)
	{
		$path = $util->deriveTemplatePathFromName($name);

		return is_file($path)
			? $util
				->createObject(WpRenderer::class)
				->setFile($path)
			: null;
	}
}

/* vi:set ts=4 sw=4 noet: */


code:
/*
Theme Name: De Houtgrutter
Description: Thema 2015
Version: 1.2
Author: ITSociaal
Author URI: http://www.itsociaal.nl/
Template: enfold
*/

@font-face {
	font-family: "Kon Tiki Enchanted JF";
	src: url("assets/KonTikiEnchantedJF-Regular.woff2") format("woff2")
	     url("assets/KonTikiEnchantedJF-Regular.woff") format("woff")
	     url("assets/KonTikiEnchantedJF-Regular.ttf") format("truetype");
	font-weight: 400;
	font-style: normal;
}

/* Oude quickcss contents */
.caption_framed .slideshow_caption .avia-caption-content p, .caption_framed .slideshow_caption .avia-caption-title, .avia-caption .avia-caption-content p, .avia-caption .avia-caption-title {
	background: rgba(95, 48, 122, 0.5);
}

p { font-size: 15px; }
h2 { font-style: italic; }
div.modern-quote h3.av-special-heading-tag { font-weight: 600; }
.openingstijden span { display: inline-block; width: 3ex; }

/* verbergt de titels van de paginas */
h1.main-title.entry-title { display: none; }
strong.main-title.entry-title { display: none; }

/* copyright info weghalen omdat dit een childtheme is */
#socket .copyright { float: none; }
span.copyright { display: table; margin: 0px auto; }

.html_stretched div#wrap_all, #main { background: #FFF; }
.html_header_top.html_header_sticky body#top div#wrap_all div#main,
.html_header_top.html_header_sticky.html_bottom_nav_header div#main,
.html_header_top.html_header_sticky div#main,
.content { padding-top: 0px; }
ul { margin-left: 0px; }
ul, ol, h1 { margin-bottom: 0px; }

/* This allows position: sticky to work */
html.responsive, .responsive body, #wrap_all { overflow-x: visible; }
#wrap_all { overflow: visible; }

#logo {
	background: #565656;
	color: #000000;
	text-align: center;
	margin-top: 1px;

	font-family: "Kon Tiki Enchanted JF";
	font-weight: 400;
	font-size: 40pt;
	line-height: 120%;
}

/* Onderwerpen op homepagina */
body.home div.flex_column>div.avia-button-wrap>a.avia-button {
	border-radius: 3px 3px 0px 0px;
}
body.home div.flex_column>div.avia-image-container>div.avia-image-container-inner>img.avia_image {
	border-radius: 0px 0px 3px 3px;
}

/* Slideshow op homepagina */
body.home #main main.content div.avia-slideshow li.av-single-slide div.avia-caption-content {
	display: none;
}
body.home #main main.content div.avia-slideshow li.av-single-slide h2.avia-caption-title {
	font-size: 5.2vw;
}
body.home #main main.content div.avia-slideshow li.av-single-slide div.avia-caption {
	top: auto;
	bottom: 0px;
	left: auto;
	padding: 0px;
}
@media only screen and (min-width: 600px) {
	body.home #main main.content div.avia-slideshow li.av-single-slide div.avia-caption-content {
		display: block;
	}
	body.home #main main.content div.avia-slideshow li.av-single-slide h2.avia-caption-title {
		font-size: 2.6vw;
	}
	body.home #main main.content div.avia-slideshow li.av-single-slide div.avia-caption {
		left: 17px;
		padding: 10px 20px;
	}
}

/* custom webfonts */
html, body, div, span, applet, object, iframe, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video, input, 	#top .input-text,	#top input[type="text"],	#top input[type="input"],	#top input[type="password"],	#top input[type="email"],	#top input[type="number"],    #top input[type="url"],    #top input[type="tel"],    #top input[type="search"],	#top textarea,	#top select {
	font-family: "Ubuntu", sans-serif;
	font-weight: 400;
}

h1,h2,h3,h4,h5,h6, .avia_iconbox_title {
	font-family: "Titillium Web", sans-serif;
	font-weight: 600;
}

/* copyright text in footer */
.copyright {
		color: #fff;
}

html>body#top>div#wrap_all>div#main>:first-child { padding-top: 35px; }
html>body#top.catalog>div#wrap_all>div#main>:first-child { padding-top: 0px; }
html>body#top.catalog>div#wrap_all>div#main main { padding-top: 35px; }

@media only screen and (max-width: 384px) {
	.responsive #top #wrap_all .container,
	#main>div>div.container {
		padding-top: 0;
		width: 98%;
		max-width: 98%;
	}
}

/* Tekst op slideshow schalen voor mobiele weergave */
h2.avia-caption-title {
	font-size: 28px;
	font-size: 2.6vw;
}

/********************************************************************\
|MENUMENUMENUMENUMENUMENUMENUMENUMENUMENUMENUMENUMENUMENUMENUMENUMENU|
\********************************************************************/
nav.menu, nav.menu a:visited, nav.menu a { color: #FFF; }
nav.menu { background: #565656; }
div.shopmenu { background: rgba(87, 37, 117, .95); }

nav.menu a:hover {
	background: transparent;
	color: #222;

	border-bottom-color: #222;
}
div.shopmenu a:hover {
	background: #7D4A9D;
	color: #FFF;

	border-bottom-color: transparent;
	border-top-color: #FFF;
}
nav.menu a:focus, nav.menu a:hover {
	text-decoration: none;
}
nav.menu a {
	/* text content */
	font-weight: 600;
	font-size: 12px;
	white-space: nowrap;
	padding: 0px 13px;

	/* box */
	display: block;
	height: 35px;
	line-height: 29px;

	/* special effects */
	transition: color .4s ease-in-out, border-color .3s ease-out, background-color .3s ease-out;
	border-bottom: 2px solid transparent;
	border-top: 2px solid transparent;
}
body.page-id-0 nav.menu div.mainmenu li.menu-item-332 a,
body.shopp-collection nav.menu div.mainmenu li.menu-item-332 a,
body.shopp-product nav.menu div.mainmenu li.menu-item-332 a,
nav.menu div.mainmenu li.current-menu-item a {
	border-bottom-color: #000;
}
nav.menu div.shopmenu li.current-menu-item a {
	border-top-color: #FFF;
}

/* Flex layout */
nav.menu ul {
	display: -webkit-flex;
	display: -ms-flex;
	display: flex;
	-webkit-flex-wrap: wrap;
	-ms-flex-wrap: wrap;
	flex-wrap: wrap;
	-webkit-justify-content: left;
	-ms-justify-content: left;
	justify-content: left;

	height: auto;
	max-height: 35px;
	overflow-y: hidden;

	transition: max-height .5s ease-in;
}

nav.menu {
	max-height: 35px;
	overflow-y: visible;
	position: relative;
	z-index: 1024;
}
nav.menu.open {
	max-height: 1024px;
}
nav.menu.open div.hidez {
	opacity: 1;
}
nav.menu.open ul {
	display: block;
	max-height: 1024px;
}
.responsive #top.page-id-0 nav.menu.open + div#main {
	margin-top: -35px;
}
nav.menu div.expander {
	position: absolute;
	top: 0;
	right: 8px;

	width: 32px;
	height: 35px;

	background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcKeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgp2ZXJzaW9uPSIxLjEiCnZpZXdCb3g9IjAgMCAzMS45OTk5OTkgMzEuOTk5OTk4IgpoZWlnaHQ9IjMyIgp3aWR0aD0iMzIiPjxnCnRyYW5zZm9ybT0idHJhbnNsYXRlKDAsLTEwMjAuMzYyMikiLz48Zz48cmVjdApyeT0iMS4zMjgxMjQ5Igp5PSIyMS45OTk5OTgiCng9IjMuOTk5OTk5IgpoZWlnaHQ9IjMuOTk5OTk5NSIKd2lkdGg9IjIzLjk5OTk5OCIKc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIvPjxyZWN0CnJ5PSIxLjMyODEyNDkiCnk9IjUuOTk5OTk4NiIKeD0iNCIKaGVpZ2h0PSIzLjk5OTk5OTUiCndpZHRoPSIyMy45OTk5OTYiCnN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjEiIC8+PHJlY3QKcnk9IjEuMzI4MTI0OSIKeT0iMTMuOTk5OTk5Igp4PSI0IgpoZWlnaHQ9IjMuOTk5OTk5NSIKd2lkdGg9IjIzLjk5OTk5NiIKc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIgLz48L2c+PC9zdmc+Cg==") center center no-repeat;
}
ul#MainMenu.menu {
	padding-right: 48px;
}

.responsive #top #wrap_all>nav.menu .container, div.shopmenu {
  width: 100%;
	max-width: 100%;
}
div.shopmenu {
	transition: opacity .4s ease;
}

/* Search box */
.html_bottom_nav_header #top #MainMenu #menu-item-search > a {
	padding-right: 13px;
}
#MainMenu .avia-search-tooltip {
	box-shadow: 0 3px 13px 0 rgba(0, 0, 0, 0.8)
}
.html_bottom_nav_header #MainMenu .avia-search-tooltip,
#MainMenu .avia-search-tooltip .avia-arrow {
	background: #592A76;
}
.html_bottom_nav_header #MainMenu .avia-search-tooltip {
	transform: translateZ(0px);
}

body.page-id-0 div.shopmenu,
body.shopp-collection div.shopmenu,
body.shopp-product div.shopmenu {
	transition: none;
	position: static;
	width: auto;
}
body.page-id-0 #main,
body.shopp-collection #main,
body.shopp-product #main {
	margin-top: 35px;
}
body.page-id-0 div.hidez,
body.shopp-collection div.hidez,
body.shopp-product div.hidez,
body.page-id-0 div.hidez aside,
body.shopp-collection div.hidez aside,
body.shopp-product div.hidez aside {
	opacity: 1;
}
div.hidez, div.hidez aside {
	opacity: 0;
}
body.page-id-0 div.hidez a,
body.shopp-collection div.hidez a,
body.shopp-product div.hidez a {
	display: block;
}
div.hidez a {
	display: none;
}

@media only screen and (min-width: 768px) {
	nav.menu {
		overflow-y: visible/

		position: -webkit-sticky;
		position: -ms-sticky;
		position: sticky;
		top: 0px;
		background: rgba(77, 77, 77, .95);
		z-index: 1024;
	}
	nav.menu ul#MainMenu {
		justify-content: center;
	}
	nav.menu ul#ShopMenu {
		left: -13px;
		justify-content: left;
	}
}
@media only screen and (min-width: 768px) and (max-width: 783px) {
	body#top nav.menu>div.shopmenu>div.container {
		padding: 0;
	}
}
@media only screen and (min-width: 784px) {
	nav.menu div.expander {
		display: none;
	}
	nav.menu.open ul {
		display: -webkit-flex;
		display: -ms-flex;
		display: flex;
	}
	ul#MainMenu.menu {
		padding-right: 0px;
	}
}
@media only screen and (min-width: 900px) {
	nav.menu li.cart-subtotal {
		display: inline;
		margin-left: auto;
	}
	nav.menu li.cart-subtotal>span.cart-subtotal {
		display: block;
		height: 35px;
		line-height: 46px;
		font-weight: 600;
		border-top: 2px solid transparent;
		border-bottom: 2px solid transparent;
	}
	nav.menu li.cart-subtotal>img {
		display: block;
		position: absolute;
		top: 50%;
		margin-top: -16px;
		margin-left: -38px;
	}
}

/* Hamburger menu search box */
nav.menu.open div.mainmenu  li div.avia-search-tooltip {
	margin-left: 0px;
  width: 90%;
  max-width: 300px;
}

/**********************************************************************\
|WEBSHOPWEBSHOPWEBSHOPWEBSHOPWEBSHOPWEBSHOPWEBSHOPWEBSHOPWEBSHOPWEBSHOP|
\**********************************************************************/
#main #shopp table { width: auto; margin: 0 auto; }
#main #shopp #cart table { clear: both; margin: 3em auto 1em auto; }
#main #shopp #cart th { padding: 0 34px 0 0; }
#main #shopp #cart th.money { padding: 0 0 0 34px; }
body.page-id-0 #main { overflow-x: scroll; }
#main #shopp #cart .item { width: auto; }
#main #shopp form#cart a.button { font-weight: bold; }

#main .main_color #shopp select,
#main .main_color #shopp input[type="text"],
#main .main_color #shopp input[type="input"],
#main .main_color #shopp input[type="email"] {
	color: #000000;
}

@media only screen and (min-width: 672px) {
	body.page-id-0 #main { overflow-x: visible; }
}

#wrap_all a.avia-post-nav.avia-post-prev,
#wrap_all a.avia-post-nav.avia-post-next {
	display: none;
}
#top #main #shopp select {
	width: auto;
}

#main #shopp h2 {
	font-style: normal;
	font-weight: 400;
	color: #5F307C;
}

#main #shopp ul.products,
#main #shopp form ul li#billing-address-fields {
  clear: left;
}
#main #shopp ul.products>li.product form {
	margin-top: 10px;
	text-align: center;
}
#main #shopp ul.products>li.product form .addtocart {
	white-space: normal;
}
#main #shopp.list ul.products li.product {
	border-bottom: none;
}
#main .shopp_grid-3 .products .product {
	width: auto;
}

#main #shopp li.product>a:hover {
	text-decoration: none;
}
#main #shopp li.product>a {
	display: block;
	position: relative;
}
#main #shopp li.product>a>img {
	display: block;
	width: 100%;
	height: auto;
	background: #CCC;
}
#main #shopp.list ul.products li img {
	float: none;
}

#main #shopp .product-details {
	padding: 1.618em;
	transition: opacity ease .4s;
	background: rgba(87, 37, 117, .8);
}
#main #shopp .product-details,
#main #shopp .product-details h3,
#main #shopp .product-details span {
	color: #FFF;
}

#main #shopp .product-details:hover {
	opacity: 1;
}
#main #shopp .price {
	display: block;
}

@media only screen and (min-width: 448px) {
	#main #shopp ul.products>li.product {
		float: left;
		width: 47%;
		margin-right: 6%;
	}
	#main #shopp ul.products>li.product:nth-child(2n+2) {
		margin-right: 0px;
	}
	#main #shopp ul.products>li.product:nth-child(2n+1) {
		clear: left;
	}
}
@media only screen and (min-width: 800px) {
	#main #shopp .product-details {
		opacity: 0;
		position: absolute;
		top: 0px;
		left: 0px;
		right: 0px;
		bottom: 0px;
	}
	#main #shopp ul.products>li.product {
		width: 29.3333%;
	}
	#main #shopp ul.products>li.product:nth-child(2n+1) {
		clear: none;
	}
	#main #shopp ul.products>li.product:nth-child(2n+2) {
		margin-right: 6%;
	}
	#main #shopp ul.products>li.product:nth-child(3n+3) {
		margin-right: 0px;
	}
	#main #shopp ul.products>li.product:nth-child(3n+1) {
		clear: left;
	}

}

#main .advertentie {
	clear: both;
	position: relative;
}
#main .advertentie-featured {
	z-index: 1;
	display: block;
	width: 100%;
	height: auto;
	min-height: 400px;
}
#main .advertentie-content-wrap {
	z-index: 2;
	position: absolute;
	top: 84px;
	left: 0px;
	bottom: 84px;
	right: 0px;
}
#main .advertentie-content-wrap>.container {
	height: 100%;
}
#main .advertentie-content {
	float: left;
	max-width: 480px;
	height: auto;
	min-height: 196px;
	max-height: 312px;
	background: rgba(255, 255, 255, 0.8);
	padding: 22px 17px 15px 75px;
	font-size: 20px;
}
#main .advertentie-content h2 {
	font-style: normal;
	color: #56297A;
	text-indent: -55px;

	font-family: "Kon Tiki Enchanted JF";
	font-weight: 400;
	font-size: 40pt;
}
#main .advertentie-content a.button:hover {
	background: #7D4A9D;
	text-decoration: none;
}
#main .advertentie-content a.button {
	background: #56297A;
	color: #FFF;
	padding: 10px 15px;
	float: right;
	margin-top: 26px;
}

@media only screen and (max-width: 672px) {
	#main .advertentie-featured {
		min-height: 320px;
	}
	#main .advertentie-content-wrap {
		top: 35px;
		right: 0px;
		bottom: 0px;
		left: 0px;
	}
	#top #wrap_all #main .advertentie-content-wrap>.container {
		width: 100%;
		max-width: 100%;
	}
	#main .advertentie-content {
		max-width: 100%;
		height: 100%;
		max-height: 100%;
	}
}

/* CART ICON THINGY */
div.shopmenu>div.container {
	text-align: right;
	position: absolute;
}
aside#cart-subtotal {
	display: inline-block;
	position: relative;
	top: -35px;
	left: 13px;
	padding-right: 8px;
}
.open aside#cart-subtotal {
	display: none;
}
aside#cart-subtotal>a>span {
	display: block;
	float: left;
	height: 35px;
	padding-right: 6.5px;

	font-weight: 600;
	line-height: 29px;

	border-top: 2px solid transparent;
	border-bottom: 2px solid transparent;
}
/*aside#cart-subtotal>a>img {
	position: relative;
	top: 2px;
}*/
.open #cart-subtotal {
  display: none;
}
.responsive #top #wrap_all #ShopMenu {
  padding-right: 128px;
}
@media only screen and (min-width: 784px) { /* zelfde als expander */
	aside#cart-subtotal {
		padding-right: 0px;
	}
}

/* vi:set ts=2 sw=2 noet: */


code:
Minetest MOD X Markeplace [x_marketplace]

This MOD for Minetest adds a simple global marketplace to the game. This marketplace is designed to work only with chat commands. Type `/mp help` in the chat to see all possible commands.

Currently only items from `default` MTGame MOD are in the store list, since all the other items can be crafted from default blocks, this is sufficient for balanced game.

Items can be added/removed/edited easily from the `store_list.lua` file.

![screenshot](screenshot.png)

Commands:

- `/mp find <item name>`, find item in store
- `/mp balance`, show your current balance in BitGold
- `/mp sellhand`, sell item(s) currently holding in hand
- `/mp buyhand [<amount>]`, buy `<amount>` of item(s) currently holding in hand, when `<amount>` is not provided then amount is 1
- `/mp infohand`, show more information about the item(s) you are currently holding in hand from the store
- `/mp buy <item name> [<amount>]`, buy `<amount>` of `<item name>` from store, if `<amount>` is not provided then amount is 1
- `/mp sellinv <item name>`, sell all items `<item name>` from the 'main' inventory list
- `/mp buyinv <item name>`, buy full inventory of items `<item name>`, empty slots in the 'main' inventory are required
- `/mp top`, show top 5 richest players currently online
- `/mp help`, print out this help


code:
--- Returns true if given value is a finite number; otherwise false or nil if value is not of type string nor number.
function x_marketplace.isfinite(value)
	if type(value) == "string" then
		value = tonumber(value)
		if value == nil then return nil end
	elseif type(value) ~= "number" then
		return nil
	end
	return value > -math.huge and value < math.huge
end

--- Returns true if given value is not a number (NaN); otherwise false or nil if value is not of type string nor number.
function x_marketplace.isnan(value)
	if type(value) == "string" then
		value = tonumber(value)
		if value == nil then return nil end
	elseif type(value) ~= "number" then
		return nil
	end
	return value ~= value
end

--- rounds a number to the nearest decimal places
-- @local
local function round(val, decimal)
	if (decimal) then
		return math.floor( (val * 10^decimal) + 0.5) / (10^decimal)
	else
		return math.floor(val+0.5)
	end
end

local function string_endswith(full, part)
	return full:find(part, 1, true) == #full - #part + 1
end

--- Normalize nodename string without mod prefix and return nodename with mod prefix. This function was borrowed from WorldEdit mod.
-- @param nodename name of the node as a string, can be without the mod prefix
-- @return nil if node doesn't exists or nodename with mode prefix
-- @see https://github.com/Uberi/Minetest-WorldEdit
function x_marketplace.normalize_nodename(nodename)
	if not nodename then
		return false
	end

	nodename = nodename:gsub("^%s*(.-)%s*$", "%1") -- strip spaces
	if nodename == "" then return nil end

	local fullname = ItemStack({name=nodename}):get_name() -- resolve aliases
	if minetest.registered_nodes[fullname] or fullname == "air" then -- full name
		return fullname
	end
	for key, value in pairs(minetest.registered_nodes) do
		if string_endswith(key, ":" .. nodename) then -- matches name (w/o mod part)
			return key
		end
	end
end

--- Search items in marketplace object
-- @param string the string
-- @return string with found items, if no items found returns boolean false
function x_marketplace.store_find(string)
	local found = ""
	local str = x_marketplace.normalize_nodename(string)

	if not str then
		return false
	end

	for k, v in pairs(x_marketplace.store_list) do
		if string.find(k, str) then
			found = found..k.." buy: "..string.format("%.2f", v.buy).." sell: "..string.format("%.2f", v.sell).."\n"
		end
	end

	if found == "" then
		found = false
	end

	return found
end

--- Get random items from the store
-- @param number the integer
-- @return string with found items
function x_marketplace.store_get_random(number)
	local keys, i, suggest = {}, 1, ""
	local how_many = number or 5

	for k, v in pairs(x_marketplace.store_list) do
		keys[i] = k
		i = i + 1
	end

	for i = 1, how_many do
		suggest = suggest.."\n"..keys[math.random(1, #keys)]
	end

	return suggest
end

--- Get players balance of BitGold
-- @param name the string of player name
-- @return string the current balance
function x_marketplace.get_player_balance(name)
	if not name then
		return ""
	end

	local player = minetest.get_player_by_name(name)
	local balance = player:get_attribute("balance") or 0
	return balance
end

--- Set players balance of BitGold
-- @param name the string of player name
-- @param amount the number of what should be added/deducted (if negative) from players balance
-- @return string the balance info message, returns false if not enough funds
function x_marketplace.set_player_balance(name, amount)
	if not name or not amount then
		return ""
	end
	local player = minetest.get_player_by_name(name)
	local balance = player:get_attribute("balance") or 0
	local new_balance = balance + amount

	if new_balance < 0 then
		return false, "below"
	end

	if new_balance > x_marketplace.max_balance then
		return false, "above"
	end

	player:set_attribute("balance", new_balance)

	return new_balance
end

function x_marketplace.find_signs(player_pos, text, sign)
	local pos0 = vector.subtract(player_pos, 2)
	local pos1 = vector.add(player_pos, 2)
	local positions = minetest.find_nodes_in_area(pos0, pos1, sign)
	local found = false

	if #positions <= 0 then
		return false
	end

	for k, v in pairs(positions) do
		local sign_meta = minetest.get_meta(v)
		local sign_text = sign_meta:get_string("text"):trim()

		if sign_text == text then
			found = true
			break
		end
	end

	return found
end

minetest.register_craftitem("x_marketplace:head", {
	description = "head",
	inventory_image = "x_marketplace_head-front.png",
	wield_image = "x_marketplace_head-front.png",
	stack_max = 1,
	wield_scale = {x=1.5, y=1.5, z=6},
	on_use = function(itemstack, user, pointed_thing)

		if pointed_thing.type == "node" then
			local node = minetest.get_node(pointed_thing.under)

			if node.name == "x_marketplace:sign_wall_bones" then
				local node_meta = minetest.get_meta(pointed_thing.under)

				if node_meta:get_string("text"):trim() == "/mp sellhead" then
					local stack_meta = itemstack:to_table().meta
					local stack_meta_value = tonumber(stack_meta.value)
					local stack_meta_owner = stack_meta.owner

					if not stack_meta_value then
						itemstack:take_item()
						return itemstack
					end

					-- sell item
					local new_balance = x_marketplace.set_player_balance(user:get_player_name(), stack_meta_value)

					if new_balance then
						minetest.sound_play("x_marketplace_gold", {
							object = user,
							max_hear_distance = 10,
							gain = 1.0
						})

						minetest.chat_send_player(user:get_player_name(), minetest.colorize(x_marketplace.colors.green, "MARKET PLACE: You sold "..stack_meta_owner.." head for "..stack_meta_value.." BitGold. Your new balance is: "..new_balance.." BitGold"))

						itemstack:take_item()
					else
						minetest.chat_send_player(user:get_player_name(), minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: You will go above the maximum balance if you sell this item. Transaction cancelled."))
					end

				end

			else
				minetest.chat_send_player(user:get_player_name(), minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: If you want to sell the head, you have to punch Bones Sign with text '/mp sellhead' on it."))
			end
		end

		return itemstack
	end,
})

minetest.register_on_dieplayer(function(player)
	local player_name = player:get_player_name()
	local balance = tonumber(x_marketplace.get_player_balance(player_name))
	local lost_value, new_balance

	if balance then
		-- no money, nothing to loose
		if balance == 0 then
			return
		-- almost no money, drop everything
		elseif balance <= 10 then
			lost_value = balance
		-- loose between 5-10% from balance
		else
			lost_value = (balance / 100) * math.random(5, 10)
		end

		lost_value = round(lost_value, 2)

		new_balance = x_marketplace.set_player_balance(player_name, lost_value * -1)

		local pos = vector.round(player:getpos())
		local itemstack = ItemStack("x_marketplace:head")
		local meta = itemstack:get_meta()
		local item_description = minetest.registered_items["x_marketplace:head"]["description"]

		meta:set_string("owner", player_name)
		meta:set_string("value", lost_value)
		meta:set_string("description", player_name.." "..item_description.."\nvalue: "..lost_value.." BitGold")

		local obj = minetest.add_item(pos, itemstack)
		if obj then
			obj:set_velocity({
				x = math.random(-10, 10) / 9,
				y = 5,
				z = math.random(-10, 10) / 9,
			})
		end

		minetest.chat_send_player(player_name, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: When you died you lost "..lost_value.." BitGold"))
	end
end)


code:
--- Minetest API method. Adds definition to minetest.registered_chatcommands.
-- @param cmd the string - commnad name
-- @param chatcommand definition the table
minetest.register_chatcommand("mp", {
	params = "<command> [<item name> <amount>]",
	description = "Marketplace commands, type '/mp help' for more help.",
	privs = { interact = true },
	func = function(caller, param)
		local params = {}
		for substring in param:gmatch("%S+") do
			table.insert(params, substring)
		end
		-- print("caller", dump(caller))
		-- print("params", dump(params))

		--
		-- find
		--
		if params[1] == "find" then
			-- item name is missing from param[2]
			if not params[2] then
				return false, minetest.colorize(x_marketplace.colors.red, "MARKET PLACE: You need to write the item name you want to find. example: /mp find default:stone. See some suggestion from the store: ")..x_marketplace.store_get_random()
			end

			local items = x_marketplace.store_find(params[2])

			if not items then
				return false, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: Oops there is no item like this in the store. Check out other items in the store: ")..x_marketplace.store_get_random()
			end

			return true, minetest.colorize(x_marketplace.colors.cyan, items)

		--
		-- show balance
		--
		elseif params[1] == "balance" then
			local balance = x_marketplace.get_player_balance(caller)

			-- check for number sanity, positive number
			if not balance or
				 x_marketplace.isnan(balance) or
				 not x_marketplace.isfinite(balance) then

				local player = minetest.get_player_by_name(caller)
				player:set_attribute("balance", 0)
				balance = 0
			end

			return true, minetest.colorize(x_marketplace.colors.green, "MARKET PLACE: Your balance is: "..balance.." BitGold")

		--
		-- sell hand
		--
		elseif params[1] == "sellhand" then
			local player = minetest.get_player_by_name(caller)

			local player_pos = player:get_pos()
			local find_signs = x_marketplace.find_signs(player:get_pos(), "/mp sell", "x_marketplace:sign_wall_mese")

			if not find_signs then
				return false, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: There are no Mese Signs around you with text '/mp sell' on them. Transaction cancelled.")
			end

			local hand = player:get_wielded_item()
			local item_name = hand:get_name()
			local store_item = x_marketplace.store_list[item_name]

			-- item exists in the store
			if store_item then
				local item_count = hand:get_count()
				local itemstack = ItemStack(item_name)

				-- check for number sanity, positive number
				if x_marketplace.isnan(item_count) or
					 not x_marketplace.isfinite(item_count) then
					item_count = 1
				end

				if item_count > itemstack:get_stack_max() then
					item_count = itemstack:get_stack_max()
				end

				local sell_price = store_item.sell * item_count
				local new_balance, msg = x_marketplace.set_player_balance(caller, sell_price)

				if msg == "above" then
					return false, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: You will go above the maximum balance if you sell this item. Transaction cancelled.")
				end

				player:set_wielded_item(ItemStack(""))

				minetest.sound_play("x_marketplace_gold", {
					object = player,
					max_hear_distance = 10,
					gain = 1.0
				})

				return true, minetest.colorize(x_marketplace.colors.green, "MARKET PLACE: You sold "..item_count.." item(s) of "..item_name.." for "..sell_price.." BitGold. Your new balance is: "..new_balance.." BitGold")
			else
			-- item does not exists in the store
				return false, minetest.colorize(x_marketplace.colors.red, "MARKET PLACE: You cannot sell this item. Search in store for items you can sell, example: /mp find stone. See some suggestion from the store: ")..x_marketplace.store_get_random()
			end

		--
		-- info hand
		--
		elseif params[1] == "infohand" then
			local player = minetest.get_player_by_name(caller)
			local hand = player:get_wielded_item()
			local item_name = hand:get_name()
			local item_count = hand:get_count()
			local store_item = x_marketplace.store_list[item_name]

			-- item exists in the store
			if store_item then
				local itemstack = ItemStack(item_name)

				if item_count > itemstack:get_stack_max() then
					item_count = itemstack:get_stack_max()
				end

				local sell_price = store_item.sell * item_count

				return true, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: "..item_name.." buy: "..store_item.buy.." sell: "..store_item.sell..". You can sell the item(s) you are holding for: "..sell_price.." BitGold. example: /mp sellhand")
			else
			-- item does not exists in the store
				return false, minetest.colorize(x_marketplace.colors.red, "MARKET PLACE: This item is not in store. See some suggestion from the store: ")..x_marketplace.store_get_random()
			end

		--
		-- buy hand
		--
		elseif params[1] == "buyhand" then
			local player = minetest.get_player_by_name(caller)

			local find_signs = x_marketplace.find_signs(player:get_pos(), "/mp buy", "x_marketplace:sign_wall_diamond")

			if not find_signs then
				return false, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: There are no Diamond Signs around you with text '/mp buy' on them. Transaction cancelled.")
			end

			local hand = player:get_wielded_item()
			local item_name = hand:get_name()
			local store_item = x_marketplace.store_list[item_name]

			-- item exists in the store
			if store_item then
				local amount = tonumber(params[2])
				local inv = player:get_inventory("main")
				local itemstack = ItemStack(item_name)

				-- check for number sanity, positive number
				if not amount or
					 x_marketplace.isnan(amount) or
					 not x_marketplace.isfinite(amount) or
					 amount <= 0 then

					amount = 1
				end

				if amount > itemstack:get_stack_max() then
					amount = itemstack:get_stack_max()
				end

				itemstack:set_count(amount)

				local buy_price = amount * store_item.buy
				local new_balance = x_marketplace.set_player_balance(caller, buy_price * -1)

				-- not enough money
				if not new_balance then
					return false, minetest.colorize(x_marketplace.colors.red, "MARKET PLACE: You don't have enought BitGold. Price for "..amount.." item(s) of "..item_name.." is "..buy_price.." BitGold, but your current balance is: "..x_marketplace.get_player_balance(caller).." BitGold")
				end

				-- drop items what doesn't fit in the inventory
				local leftover_item = inv:add_item("main", itemstack)
				if leftover_item:get_count() > 0 then
					local p = table.copy(player:get_pos())
					p.y = p.y + 1.2
					local obj = minetest.add_item(p, leftover_item)

					if obj then
						local dir = player:get_look_dir()
						dir.x = dir.x * 2.9
						dir.y = dir.y * 2.9 + 2
						dir.z = dir.z * 2.9
						obj:set_velocity(dir)
						obj:get_luaentity().dropped_by = caller
					end
				end

				minetest.sound_play("x_marketplace_gold", {
					object = player,
					max_hear_distance = 10,
					gain = 1.0
				})

				return true, minetest.colorize(x_marketplace.colors.green, "MARKET PLACE: You bought "..amount.." item(s) of "..item_name.." for "..buy_price.." BitGold. Your new balance is: "..new_balance.." BitGold")
			else
			-- item does not exists in the store
				return false, minetest.colorize(x_marketplace.colors.red, "MARKET PLACE: This item is not in store. See some suggestion from the store: ")..x_marketplace.store_get_random()
			end

		--
		-- sell inventory
		--
		elseif params[1] == "sellinv" then
			params[2] = x_marketplace.normalize_nodename(params[2])

			-- item name is missing from param[2]
			if not params[2] then
				return false, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: You need to write the item name you want to sell. example: /mp sellinv default:stone. See some suggestion from the store: ")..x_marketplace.store_get_random()
			end

			local player = minetest.get_player_by_name(caller)
			local find_signs = x_marketplace.find_signs(player:get_pos(), "/mp sell", "x_marketplace:sign_wall_mese")

			if not find_signs then
				return false, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: There are no Mese Signs around you with text '/mp sell' on them. Transaction cancelled.")
			end

			local store_item = x_marketplace.store_list[params[2]]

			-- item exists in the store
			if store_item then
				local inv = player:get_inventory("main")
				local itemstack = ItemStack(params[2])
				local balance = x_marketplace.get_player_balance(caller)
				local amount = 0
				local over_max_balance = false

				for k, v in ipairs(inv:get_list("main")) do
					if v:get_name() == params[2] then
						local amount_to_remove = v:get_count()

						if amount_to_remove > itemstack:get_stack_max() then
							amount_to_remove = itemstack:get_stack_max()
						end

						amount = amount + amount_to_remove

						if (balance + amount * store_item.sell) > x_marketplace.max_balance then
							amount = amount - amount_to_remove
							over_max_balance = true
							break
						else
							inv:remove_item("main", v)
						end
					end
				end

				local sell_price = amount * store_item.sell
				local new_balance = x_marketplace.set_player_balance(caller, sell_price)

				if amount == 0 then
					return false, minetest.chat_send_player(caller, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: You have nothing to sell in your inventory."))
				end

				if over_max_balance then
					minetest.chat_send_player(caller, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: We couldn't buy all your items without going above your maximum balance."))
				end

				minetest.sound_play("x_marketplace_gold", {
					object = player,
					max_hear_distance = 10,
					gain = 1.0
				})

				return true, minetest.colorize(x_marketplace.colors.green, "MARKET PLACE: You sold "..amount.." item(s) of "..params[2].." for "..sell_price.." BitGold. Your new balance is: "..x_marketplace.get_player_balance(caller).." BitGold")
			else
			-- item does not exists in the store
				return false, minetest.colorize(x_marketplace.colors.red, "MARKET PLACE: You cannot sell this item. Search in store for items you can sell, example: /mp find stone. See some suggestion from the store: ")..x_marketplace.store_get_random()
			end

		--
		-- buy inventory
		--
		elseif params[1] == "buyinv" then
			params[2] = x_marketplace.normalize_nodename(params[2])

			-- item name is missing from param[2]
			if not params[2] then
				return false, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: You need to write the item name you want to buy. example: /mp buyinv default:stone, or check out other items in the store: ")..x_marketplace.store_get_random()
			end

			local player = minetest.get_player_by_name(caller)
			local find_signs = x_marketplace.find_signs(player:get_pos(), "/mp buy", "x_marketplace:sign_wall_diamond")

			if not find_signs then
				return false, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: There are no Diamond Signs around you with text '/mp buy' on them. Transaction cancelled.")
			end

			local store_item = x_marketplace.store_list[params[2]]

			if store_item then
				local itemstack = ItemStack(params[2])
				local inv = player:get_inventory("main")
				local amount = 0
				itemstack:set_count(itemstack:get_stack_max())

				for k, v in ipairs(inv:get_list("main")) do
					if v:get_name() == "" and v:get_count() == 0 then
						amount = amount + itemstack:get_count()
					end
				end

				if amount == 0 then
					return false, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: You don't have empty space in your inventory. Transaction cancelled.")
				end

				local buy_price = amount * store_item.buy
				local new_balance = x_marketplace.set_player_balance(caller, buy_price * -1)

				-- not enough money
				if not new_balance then
					return false, minetest.colorize(x_marketplace.colors.red, "MARKET PLACE: You don't have enought BitGold. Price for "..amount.." item(s) of "..params[2].." is "..buy_price.." BitGold, but your current balance is: "..x_marketplace.get_player_balance(caller).." BitGold.")
				end

				for k, v in ipairs(inv:get_list("main")) do
					if v:get_name() == "" and v:get_count() == 0 then
						inv:add_item("main", itemstack)
					end
				end

				minetest.sound_play("x_marketplace_gold", {
					object = player,
					max_hear_distance = 10,
					gain = 1.0
				})

				return true, minetest.colorize(x_marketplace.colors.green, "MARKET PLACE: You bought "..amount.." item(s) of "..params[2].." for "..buy_price.." BitGold. Your new balance is: "..new_balance.." BitGold")

			else
			-- item not in store
				return false, minetest.colorize(x_marketplace.colors.red, "MARKET PLACE: This item is not in store, check out other items from the store: ")..x_marketplace.store_get_random()
			end

		--
		-- buy
		--
		elseif params[1] == "buy" then
			local amount = tonumber(params[3])
			params[2] = x_marketplace.normalize_nodename(params[2])

			-- check for param[2] - item name
			if not params[2] then
				return false, minetest.colorize(x_marketplace.colors.red, "MARKET PLACE: You need to write the item name you want to buy. example: /mp buy default:stone 10, or check out other items in the store: ")..x_marketplace.store_get_random()
			end

			-- find sign
			local player = minetest.get_player_by_name(caller)
			local find_signs = x_marketplace.find_signs(player:get_pos(), "/mp buy", "x_marketplace:sign_wall_diamond")

			if not find_signs then
				return false, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: There are no Diamond Signs around you with text '/mp buy' on them. Transaction cancelled.")
			end

			-- item not in store
			if not x_marketplace.store_list[params[2]] then
				return false, minetest.colorize(x_marketplace.colors.yellow, "MARKET PLACE: This item is not in store, check out other items from the store: ")..x_marketplace.store_get_random()
			end

			-- check for param[3] - amount
			local itemstack = ItemStack(params[2])

			-- check for number sanity, positive number
			if not amount or
				 x_marketplace.isnan(amount) or
				 not x_marketplace.isfinite(amount) or
				 amount <= 0 then

				amount = 1
			end

			if amount > itemstack:get_stack_max() then
				amount = itemstack:get_stack_max()
			end

			itemstack:set_count(amount)

			-- add items to main inventory
			local store_item = x_marketplace.store_list[params[2]]
			local inv = player:get_inventory("main")

			local buy_price = amount * store_item.buy
			local new_balance = x_marketplace.set_player_balance(caller, buy_price * -1)

			-- not enough money
			if not new_balance then
				return false, minetest.colorize(x_marketplace.colors.red, "MARKET PLACE: You don't have enought BitGold. Price for "..amount.." item(s) of "..params[2].." is "..buy_price.." BitGold, but your current balance is: "..x_marketplace.get_player_balance(caller).." BitGold")
			end

			-- drop items what doesn't fit in the inventory
			local leftover_item = inv:add_item("main", itemstack)
			if leftover_item:get_count() > 0 then
				local p = table.copy(player:get_pos())
				p.y = p.y + 1.2
				local obj = minetest.add_item(p, leftover_item)

				if obj then
					local dir = player:get_look_dir()
					dir.x = dir.x * 2.9
					dir.y = dir.y * 2.9 + 2
					dir.z = dir.z * 2.9
					obj:set_velocity(dir)
					obj:get_luaentity().dropped_by = caller
				end
			end

			minetest.sound_play("x_marketplace_gold", {
				object = player,
				max_hear_distance = 10,
				gain = 1.0
			})

			return true, minetest.colorize(x_marketplace.colors.green, "MARKET PLACE: You bought "..amount.." item(s) of "..params[2].." for "..buy_price.." BitGold. Your new balance is: "..new_balance.." BitGold")

		--
		-- top 5 richest
		--
		elseif params[1] == "top" then
			local players = minetest.get_connected_players()
			local temp_tbl = {}

			for k, v in ipairs(players) do
				local pname = v:get_player_name()
				local balance = x_marketplace.get_player_balance(pname)
				table.insert(temp_tbl, { name = pname, balance = tonumber(balance) })
			end

			table.sort(temp_tbl, function(a, b) return a.balance > b.balance end)

			local msg = "MARKET PLACE: \n"
			local length = 5

			if length > #temp_tbl then
				length = #temp_tbl
			end

			for i = 1, length do
				msg = msg..i..". "..temp_tbl[i].name.."\n"
			end

			-- print(dump(temp_tbl))
			return true, minetest.colorize(x_marketplace.colors.yellow, msg)

		--
		-- help
		--
		elseif params[1] == "help" then
			local msg =
			minetest.colorize(x_marketplace.colors.cyan, "/mp find").." <item name>, find item in store\n"..
			minetest.colorize(x_marketplace.colors.cyan, "/mp balance")..", show your current balance in BitGold\n"..
			minetest.colorize(x_marketplace.colors.cyan, "/mp sellhand")..", sell item(s) currently holding in hand, you must be near Mese Sign with text '/mp sell' on it\n"..
			minetest.colorize(x_marketplace.colors.cyan, "/mp buyhand").." [<amount>], buy <amount> of item(s) currently holding in hand, when <amount> is not provided then amount is 1, you must be near Diamond Sign with text '/mp buy' on it\n"..
			minetest.colorize(x_marketplace.colors.cyan, "/mp infohand")..", show more information about the item(s) you are currently holding in hand from the store\n"..
			minetest.colorize(x_marketplace.colors.cyan, "/mp buy").." <item name> [<amount>], buy <amount> of <item name> from store, if <amount> is not provided then amount is 1, you must be near Diamond Sign with text '/mp buy' on it\n"..
			minetest.colorize(x_marketplace.colors.cyan, "/mp sellinv").." <item name>, sell all items <item name> from the 'main' inventory list, you must be near Mese Sign with text '/mp sell' on it\n"..
			minetest.colorize(x_marketplace.colors.cyan, "/mp buyinv").." <item name>, buy full inventory of items <item name>, empty slots in the 'main' inventory are required, you must be near Diamond Sign with text '/mp buy' on it\n"..
			minetest.colorize(x_marketplace.colors.cyan, "/mp top")..", show top 5 richest players currently online\n"..
			minetest.colorize(x_marketplace.colors.cyan, "/mp help")..", print out this help\n"

			-- print(msg)
			return true, msg
		end
	end
})


code:
This MOD for Minetest adds a simple global marketplace to the game. This markeplace is designed to work only with chat commands. Type mp help in the chat to see all possible commands.

Currently only items from default MTGame MOD are in the store list, since all the other items can be crafted from default blocks, this is sufficient for balanced game.


code:
-- Definitions made by this mod that other mods can use too
-- @module x_marketplace
-- @author SaKeL
x_marketplace = {}
x_marketplace.max_balance = 1000000 -- one million
x_marketplace.colors = {
	["yellow"] = "#FFEB3B", -- info
	["green"] = "#4CAF50", -- success
	["red"] = "#f44336", -- error
	["cyan"] = "#00BCD4" -- terminal info
}

local path = minetest.get_modpath("x_marketplace")

dofile(path.."/store_list.lua")
dofile(path.."/api.lua")
dofile(path.."/chatcommands.lua")
dofile(path.."/nodes.lua")

print ("[Mod] x_marketplace loaded")


code:
--
-- Bones sign (sellhead)
--
minetest.register_node("x_marketplace:sign_wall_bones", {
	description = "Bones Sign - write '/mp sellhead' to sell heads",
	drawtype = "nodebox",
	tiles = {"x_marketplace_sign_wall_bones.png"},
	inventory_image = "x_marketplace_sign_bones.png",
	wield_image = "x_marketplace_sign_bones.png",
	paramtype = "light",
	paramtype2 = "wallmounted",
	sunlight_propagates = true,
	is_ground_content = false,
	walkable = false,
	node_box = {
		type = "wallmounted",
		wall_top    = {-0.4375, 0.4375, -0.3125, 0.4375, 0.5, 0.3125},
		wall_bottom = {-0.4375, -0.5, -0.3125, 0.4375, -0.4375, 0.3125},
		wall_side   = {-0.5, -0.3125, -0.4375, -0.4375, 0.3125, 0.4375},
	},
	groups = {crumbly = 2},
	sounds = default.node_sound_gravel_defaults(),

	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("formspec", "field[text;;${text}]")
	end,
	on_receive_fields = function(pos, formname, fields, sender)
		--print("Sign at "..minetest.pos_to_string(pos).." got "..dump(fields))
		local player_name = sender:get_player_name()
		if minetest.is_protected(pos, player_name) then
			minetest.record_protection_violation(pos, player_name)
			return
		end
		local meta = minetest.get_meta(pos)
		if not fields.text then return end
		minetest.log("action", (player_name or "") .. " wrote \"" ..
			fields.text .. "\" to sign at " .. minetest.pos_to_string(pos))
		meta:set_string("text", fields.text)
		meta:set_string("infotext", '"' .. fields.text .. '"')
	end,
})

--
-- Mese sign (sell)
--
minetest.register_node("x_marketplace:sign_wall_mese", {
	description = "Mese Sign - write '/mp sell' to sell to marketplace",
	drawtype = "nodebox",
	tiles = {"x_marketplace_sign_wall_mese.png"},
	inventory_image = "x_marketplace_sign_mese.png",
	wield_image = "x_marketplace_sign_mese.png",
	paramtype = "light",
	paramtype2 = "wallmounted",
	sunlight_propagates = true,
	is_ground_content = false,
	walkable = false,
	node_box = {
		type = "wallmounted",
		wall_top    = {-0.4375, 0.4375, -0.3125, 0.4375, 0.5, 0.3125},
		wall_bottom = {-0.4375, -0.5, -0.3125, 0.4375, -0.4375, 0.3125},
		wall_side   = {-0.5, -0.3125, -0.4375, -0.4375, 0.3125, 0.4375},
	},
	groups = {cracky = 1, level = 2},
	sounds = default.node_sound_stone_defaults(),

	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("formspec", "field[text;;${text}]")
	end,
	on_receive_fields = function(pos, formname, fields, sender)
		--print("Sign at "..minetest.pos_to_string(pos).." got "..dump(fields))
		local player_name = sender:get_player_name()
		if minetest.is_protected(pos, player_name) then
			minetest.record_protection_violation(pos, player_name)
			return
		end
		local meta = minetest.get_meta(pos)
		if not fields.text then return end
		minetest.log("action", (player_name or "") .. " wrote \"" ..
			fields.text .. "\" to sign at " .. minetest.pos_to_string(pos))
		meta:set_string("text", fields.text)
		meta:set_string("infotext", '"' .. fields.text .. '"')
	end,
})

--
-- Diamond sign (buy)
--
minetest.register_node("x_marketplace:sign_wall_diamond", {
	description = "Diamond Sign - write '/mp buy' to buy from marketplace",
	drawtype = "nodebox",
	tiles = {"x_marketplace_sign_wall_diamond.png"},
	inventory_image = "x_marketplace_sign_diamond.png",
	wield_image = "x_marketplace_sign_diamond.png",
	paramtype = "light",
	paramtype2 = "wallmounted",
	sunlight_propagates = true,
	is_ground_content = false,
	walkable = false,
	node_box = {
		type = "wallmounted",
		wall_top    = {-0.4375, 0.4375, -0.3125, 0.4375, 0.5, 0.3125},
		wall_bottom = {-0.4375, -0.5, -0.3125, 0.4375, -0.4375, 0.3125},
		wall_side   = {-0.5, -0.3125, -0.4375, -0.4375, 0.3125, 0.4375},
	},
	groups = {cracky = 1, level = 3},
	sounds = default.node_sound_stone_defaults(),

	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("formspec", "field[text;;${text}]")
	end,
	on_receive_fields = function(pos, formname, fields, sender)
		--print("Sign at "..minetest.pos_to_string(pos).." got "..dump(fields))
		local player_name = sender:get_player_name()
		if minetest.is_protected(pos, player_name) then
			minetest.record_protection_violation(pos, player_name)
			return
		end
		local meta = minetest.get_meta(pos)
		if not fields.text then return end
		minetest.log("action", (player_name or "") .. " wrote \"" ..
			fields.text .. "\" to sign at " .. minetest.pos_to_string(pos))
		meta:set_string("text", fields.text)
		meta:set_string("infotext", '"' .. fields.text .. '"')
	end,
})

--
-- CRAFTING
--

minetest.register_craft({
	output = "x_marketplace:sign_wall_bones",
	recipe = {
		{"bones:bones", "bones:bones", "bones:bones"},
		{"bones:bones", "bones:bones", "bones:bones"},
		{"", "group:stick", ""},
	}
})

minetest.register_craft({
	output = "x_marketplace:sign_wall_mese",
	recipe = {
		{"default:mese_crystal", "default:mese_crystal", "default:mese_crystal"},
		{"default:mese_crystal", "default:mese_crystal", "default:mese_crystal"},
		{"", "group:stick", ""},
	}
})

minetest.register_craft({
	output = "x_marketplace:sign_wall_diamond",
	recipe = {
		{"default:diamond", "default:diamond", "default:diamond"},
		{"default:diamond", "default:diamond", "default:diamond"},
		{"", "group:stick", ""},
	}
})


code:
x_marketplace.store_list = {
	-- default
	["default:acacia_bush_leaves"] = {
		buy = 0.40,
		sell = 0.10
	},
	["default:acacia_bush_sapling"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:acacia_bush_stem"] = {
		buy = 0.40,
		sell = 0.10
	},
	["default:acacia_leaves"] = {
		buy = 0.40,
		sell = 0.10
	},
	["default:acacia_sapling"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:acacia_tree"] = {
		buy = 8.00,
		sell = 2.00
	},
	["default:acacia_wood"] = {
		buy = 2.00,
		sell = 0.50
	},
	["default:apple"] = {
		buy = 40.00,
		sell = 10.00
	},
	["default:aspen_leaves"] = {
		buy = 0.40,
		sell = 0.10
	},
	["default:aspen_sapling"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:aspen_tree"] = {
		buy = 8.00,
		sell = 2.00
	},
	["default:aspen_wood"] = {
		buy = 2.00,
		sell = 0.50
	},
	["default:bookshelf"] = {
		buy = 240.00,
		sell = 60.00
	},
	["default:brick"] = {
		buy = 104.00,
		sell = 26.00
	},
	-- ["default:bronzeblock"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	["default:bush_leaves"] = {
		buy = 0.40,
		sell = 0.10
	},
	["default:bush_sapling"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:bush_stem"] = {
		buy = 0.40,
		sell = 0.10
	},
	["default:cactus"] = {
		buy = 20.00,
		sell = 5.00
	},
	["default:chest"] = {
		buy = 16.00,
		sell = 4.00
	},
	-- ["default:chest_locked"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	["default:clay"] = {
		buy = 10.00,
		sell = 2.50
	},
	-- ["default:coalblock"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	["default:cobble"] = {
		buy = 1.00,
		sell = 0.25
	},
	-- ["default:copperblock"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:coral_brown"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:coral_orange"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	["default:coral_skeleton"] = {
		buy = 8.00,
		sell = 2.00
	},
	["default:desert_cobble"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:desert_sand"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:desert_sandstone"] = {
		buy = 4.00,
		sell = 1.00
	},
	-- ["default:desert_sandstone_block"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:desert_sandstone_brick"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	["default:desert_stone"] = {
		buy = 2.00,
		sell = 0.50
	},
	-- ["default:desert_stone_block"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:desert_stonebrick"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:diamondblock"] = {
	-- 	buy = 16200.00,
	-- 	sell = 1.00
	-- },
	["default:dirt"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:dirt_with_coniferous_litter"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:dirt_with_dry_grass"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:dirt_with_grass"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:dirt_with_rainforest_litter"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:dirt_with_snow"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:dry_grass_5"] = {
		buy = 2.00,
		sell = 0.50
	},
	["default:dry_shrub"] = {
		buy = 4.00,
		sell = 1.00
	},
	["default:fence_acacia_wood"] = {
		buy = 3.00,
		sell = 0.75
	},
	["default:fence_aspen_wood"] = {
		buy = 3.00,
		sell = 0.75
	},
	["default:fence_junglewood"] = {
		buy = 3.00,
		sell = 0.75
	},
	["default:fence_pine_wood"] = {
		buy = 3.00,
		sell = 0.75
	},
	["default:fence_wood"] = {
		buy = 3.00,
		sell = 0.75
	},
	["default:fern_3"] = {
		buy = 4.00,
		sell = 1.00
	},
	["default:furnace"] = {
		buy = 8.00,
		sell = 2.00
	},
	["default:glass"] = {
		buy = 17.00,
		sell = 4.25
	},
	["default:goldblock"] = {
		buy = 8100.00,
		sell = 1.00
	},
	["default:grass_5"] = {
		buy = 2.00,
		sell = 0.50
	},
	["default:gravel"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:ice"] = {
		buy = 20.00,
		sell = 5.00
	},
	["default:junglegrass"] = {
		buy = 4.00,
		sell = 1.00
	},
	["default:jungleleaves"] = {
		buy = 0.40,
		sell = 0.10
	},
	["default:junglesapling"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:jungletree"] = {
		buy = 8.00,
		sell = 2.00
	},
	["default:junglewood"] = {
		buy = 2.00,
		sell = 0.50
	},
	["default:ladder_steel"] = {
		buy = 2.33,
		sell = 0.58
	},
	["default:ladder_wood"] = {
		buy = 2.33,
		sell = 0.58
	},
	["default:leaves"] = {
		buy = 0.40,
		sell = 0.10
	},
	["default:marram_grass_3"] = {
		buy = 4.00,
		sell = 1.00
	},
	-- ["default:mese"] = {
	-- 	buy = 16200.00,
	-- 	sell = 1.00
	-- },
	["default:mese_post_light"] = {
		buy = 128.00,
		sell = 0.25
	},
	["default:meselamp"] = {
		buy = 128.00,
		sell = 0.50
	},
	["default:mossycobble"] = {
		buy = 4.00,
		sell = 1.00
	},
	["default:obsidian"] = {
		buy = 200.00,
		sell = 50.00
	},
	-- ["default:obsidian_block"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	["default:obsidian_glass"] = {
		buy = 17.00,
		sell = 4.25
	},
	-- ["default:obsidianbrick"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	["default:papyrus"] = {
		buy = 12.00,
		sell = 3.00
	},
	["default:permafrost"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:permafrost_with_moss"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:permafrost_with_stones"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:pine_needles"] = {
		buy = 0.40,
		sell = 0.10
	},
	["default:pine_sapling"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:pine_tree"] = {
		buy = 8.00,
		sell = 2.00
	},
	["default:pine_wood"] = {
		buy = 2.00,
		sell = 0.50
	},
	["default:sand"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:sand_with_kelp"] = {
		buy = 4.00,
		sell = 0.10
	},
	["default:sandstone"] = {
		buy = 4.00,
		sell = 1.00
	},
	-- ["default:sandstone_block"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:sandstonebrick"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	["default:sapling"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:sign_wall_steel"] = {
		buy = 4.33,
		sell = 1.08
	},
	["default:sign_wall_wood"] = {
		buy = 4.33,
		sell = 1.08
	},
	["default:silver_sand"] = {
		buy = 1.00,
		sell = 0.25
	},
	["default:silver_sandstone"] = {
		buy = 4.00,
		sell = 1.00
	},
	-- ["default:silver_sandstone_block"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:silver_sandstone_brick"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	["default:snowblock"] = {
		buy = 0.80,
		sell = 0.20
	},
	-- ["default:steelblock"] = {
	-- 	buy = 2430.00,
	-- 	sell = 1
	-- },
	["default:stone"] = {
		buy = 2.00,
		sell = 0.50
	},
	-- ["default:stone_block"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:stone_with_coal"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:stone_with_copper"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:stone_with_diamond"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:stone_with_gold"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:stone_with_iron"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:stone_with_mese"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	-- ["default:stone_with_tin"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	["default:stonebrick"] = {
		buy = 2.00,
		sell = 0.50
	},
	-- ["default:tinblock"] = {
	-- 	buy = 0.00,
	-- 	sell = 0.00
	-- },
	["default:torch"] = {
		buy = 4.25,
		sell = 1.06
	},
	["default:tree"] = {
		buy = 8.00,
		sell = 2.00
	},
	["default:wood"] = {
		buy = 8.00,
		sell = 2.00
	},
}

-- minetest.after(0, function()
-- 	for k, v in pairs(minetest.registered_nodes) do
-- 		print(k)
-- 	end
-- end)


code:
# cam11 camera library

## Introduction

This is a simple, low-level camera library that makes use of the
[Transform](https://love2d.org/wiki/Transform) object introduced in Löve
(love2d) version 11.0.

It is intended as either a simple camera for games that don't need more, or as
a basis for a more sophisticated camera handling library.

It does not provide any clamping, usually needed for games where the map should
completely cover the viewport even when reaching a border.

It was designed with speed in mind. While camera setup is usually something
that does not require speed, as it's typically performed only once per frame,
operations like transforming points to screen or world coordinates can
potentially be used many times.

This library uses a lazy approach, meaning it does not perform any unnecessary
updates of the transformation. For this to work as expected, it's important
not to access the fields directly unless you understand the consequences, and
use the accessor methods instead.

## Definitions

Here we use the term "screen" to refer to the drawing area, i.e. the drawable
area within the window, which in fullscreen mode, matches the whole physical
screen, but in general it does not necessarily do so.

This camera library is made to draw in a viewport. The viewport is any
rectangular area of the screen, or it can cover the whole screen, which is the
default. This viewport is always defined in screen coordinates. You can choose
whether to apply clipping to the viewport (using the Löve scissor) when
attaching the camera; by default, it only clips when the viewport is not set to
cover the whole screen.

There seems to be some confusion when defining coordinate systems.

When working with a map, we're using coordinates relative to an origin in the
map. For example, a 4096x4096 map can use an origin located at the top left
corner of the map, and then a point like (2000, 1000) will be 2000 units
(pixels, usually) to the east, and 1000 units to the south of that point. This
point (2000, 1000) is said to be in *world coordinates*. The term "world" is
used instead of "map" because it is more general: what you're drawing might not
be a map, but it's usually a world of some kind. The `love.physics` module, for
example, uses the same term. If you're drawing a map, the terms "world
coordinates" and "map coordinates" are equivalent.

When drawing e.g. a map to the screen using the camera, one point of the map
located, say, at coordinate (2000, 1000), does not necessarily end up in the
screen at the same coordinate. For example, if the camera is centred on that
point in an 800x600 screen using a full screen viewport, the final positon on
the screen of that point will be (400, 300). This (400, 300) is relative to the
origin of the screen, which is the top left corner. The points that use the
top left corner of the screen as origin, and use the display units, are said to
be expressed in *screen coordinates*. Löve uses this coordinate system, for
example, for reporting the mouse and touch position, for the scissor functions,
and for drawing immediately at the start of `love.draw()` or after using
`love.graphics.origin()`. This camera library uses these coordinates for
defining the viewport.

Some libraries use the term "camera coordinates". We find that term confusing,
because the camera deals with both types of coordinates, therefore we avoid it
and use only screen coordinates, for the coordinates of points of the screen,
and world coordinates, for the coordinates of the world before it is drawn
through the camera.

The library includes functions for converting coordinates from screen to world
and vice versa. A typical use is to determine the position in the world of the
mouse, because the mouse position is reported in screen coordinates, and it's
often necessary to determine where in the world the click happened.

## Usage example

This is an **incomplete** example which invokes some made up functions you
could be using in your program, and that aren't defined in this snippet:

```
local cam

function love.load()
  cam = require 'cam11'()
  cam:setZoom(2)
end

function love.update(dt)
  player:update(dt)
  cam:setPos(player:getPos())
end

function love.mousepressed(x, y, b)
  local xw, yw = cam:toWorld(x, y)
  map:mousepressed(xw, yw, b)
end

function love.draw()
  cam:attach()
  map:draw()
  player:draw()
  cam:detach()
end
```

## Reference

`local Camera = require 'cam11'`: Returns the Camera class.

`local cam = Camera.new([x], [y], [zoom], [angle], [viewport_x], [viewport_y], [viewport_w], [viewport_h], [focus_x], [focus_y])`:
Create a new camera instance object at the given position, zoom and angle,
using the given viewport. Remember that it's a class method, therefore the dot
syntax must be used, instead of the colon syntax.

`x` and `y` are the position the camera must point to, in world coordinates.
Both default to 0.

`zoom` is the magnification factor. For example, a value of 3 means to magnify
the drawing 3X. The default is 1.

`angle` is the rotation angle, in radians, relative to the focus point.
Following LÖVE's axis conventions, positive values mean clockwise rotation. The
default is 0.

`viewport_x` and `viewport_y` are the coordinates of the top left corner of the
viewport where the camera will be rendered, in screen coordinates. The default
is 0 for both.

`viewport_w` and `viewport_h` are the viewport's width and height. A value of
`false` (the default) or `nil` means to use the current screen width or height
for the corresponding parameter. Note that when using the current width or
height, the current transformation must be invalidated when the screen size
changes, by calling `cam:setDirty()` (described below), such that when it's
needed, it's regenerated with the new width and height. This invalidation can
be done in the `love.resize` and `love.displayrotated` (for 11.3+) events.

The `focus_x` and `focus_y` parameters are the fraction of the viewport's width
and height, respectively, that the camera should point to. Both default to 0.5,
meaning the centre of the viewport.

`local cam = Camera(...)`: Alternative C++-like syntax for `Camera.new(...)`.

The following are methods of the instance, described using `cam` as the name of
the instance:

`cam:setDirty([dirty])`: A value of `true` (default) invalidates the
transformation, meaning it will be regenerated the next time it's needed.
A value of `false` means the transformation will not be regenerated the next
time it's needed, which can potentially lead to invalid results based on an
obsolete transformation. Don't set it to `false` unless you understand the
consequences.

`cam:attach([clip])`: Replaces the LÖVE transformation with the current camera
parameters, remembering the old transformation in order to restore it later.
`cam:attach()` must have one corresponding `cam:detach()`. Always call
`cam:detach()` before calling `cam:attach()` again.

The optional `clip` parameter specifies whether to use the LÖVE scissor to clip
to the viewport. A value of `false` indicates not to clip; otherwise it clips.
The default is to clip unless the viewport is set to follow the screen width
and height. The clipping intersects the current scissor, rather than replace
it, to be GUI-friendly. If clipping was performed, the scissor will be restored
when calling `cam:detach()`.

`cam:detach()`: Restores the LÖVE transformation that was active since the
latest `cam:attach()`, and if clipping was performed, also restores the scissor
that was active. You must call `cam:attach()` before calling this function.

`cam:setPos(x, y)`: Changes the current *x* and *y* coordinates that the camera
must point to.

`cam:setZoom(zoom)`: Changes the current zoom of the camera.

`cam:setAngle(angle)`: Changes the current angle of the camera.

`cam:setViewport([x], [y], [w], [h], [fx], [fy])`: Changes the current
viewport's left and top coordinates, width, height, fraction of focus point
horizontal and vertical, respectively. See `Camera.new()` for details.

`cam:toScreen(x, y)`: Transforms world coordinates to screen coordinates,
returning the result.

`cam:toWorld(x, y)`: Transforms screen coordinates to world coordinates,
returning the result.

`cam:getTransform()`: Returns the current internal `Transform` object.

`cam:getPos()`: Returns the current *x* and *y* coordinates the camera is set
to point at, in world coordinates.

`cam:getX()`: Like above, but it returns only the *x* coordinate.

`cam:getY()`: Like above, but it returns only the *y* coordinate.

`cam:getZoom()`: Returns the current zoom value.

`cam:getAngle()`: Returns the angle that the camera is set to, in radians.

`cam:getViewport()`: Returns the x, y, width, height, focus X fraction, focus Y
fraction of the current viewport.

`cam:getVPTopLeft()`: Returns the top left coordinate, in screen coordinates,
of the current viewport. Equivalent to *x* and *y* of `cam:getViewport()`.

`cam:getVPBottomRight()`: Returns the bottom right coordinate, in screen
coordinates, of the current viewport.

`cam:getVPFocusPoint()`: Returns the focus point of the camera, in screen
coordinates.

Note that in order to obtain the current viewport rectangle in world
coordinates, or the axis-aligned bounding rectangle that encloses it, you can
use the functions below, as follows:

```
-- Return left, top, right bottom of the bounding rectangle of the rotated
-- viewport, in world coordinates
local min = math.min
local max = math.max

local function getVPRect(cam)
  local left, top = cam:getVPTopLeft()
  local right, bottom = cam:getVPBottomRight()
  local x1, y1 = cam:toWorld(left, top)
  local x2, y2 = cam:toWorld(right, top)
  local x3, y3 = cam:toWorld(right, bottom)
  local x4, y4 = cam:toWorld(left, bottom)
  return x1, y1, x2, y2, x3, y3, x4, y4
end

local function getBoundingVPRect(cam)
  local x1, y1, x2, y2, x3, y3, x4, y4 = getVPRect(cam)
  return min(min(x1, x2), min(x3, x4)), min(min(y1, y2), min(y3, y4)),
         max(max(x1, x2), max(x3, x4)), max(max(y1, y2), max(y3, y4))
end
```

## Feedback

Have you found a problem? Please let me know so I can fix it! It helps me a lot
if you report any issues you find. You're getting this library for free, after
all, so why not pay me back this way? That's what keeps the free software world
rolling: mutual help.

https://notabug.org/pgimeno/cam11/issues

Have any feedback? You can post in the love2d forums:

https://love2d.org/forums/viewtopic.php?f=5&t=8766

## License

This library is distributed under the following license terms:

Copyright © 2019 Pedro Gimeno Fortea

You can do whatever you want with this software, under the sole condition
that this notice and any copyright notices are preserved. It is offered
with no warrany, not even implied.