/*! * cssmin.php v2.4.8-4 * Author: Tubal Martin - http://tubalmartin.me/ * Repo: https://github.com/tubalmartin/YUI-CSS-compressor-PHP-port * * This is a PHP port of the CSS minification tool distributed with YUICompressor, * itself a port of the cssmin utility by Isaac Schlueter - http://foohack.com/ * Permission is hereby granted to use the PHP version under the same * conditions as the YUICompressor. */ /*! * YUI Compressor * http://developer.yahoo.com/yui/compressor/ * Author: Julien Lecomte - http://www.julienlecomte.net/ * Copyright (c) 2013 Yahoo! Inc. All rights reserved. * The copyrights embodied in the content of this file are licensed * by Yahoo! Inc. under the BSD (revised) open source license. */ class CSSmin { const NL = '___YUICSSMIN_PRESERVED_NL___'; const TOKEN = '___YUICSSMIN_PRESERVED_TOKEN_'; const COMMENT = '___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_'; const CLASSCOLON = '___YUICSSMIN_PSEUDOCLASSCOLON___'; const QUERY_FRACTION = '___YUICSSMIN_QUERY_FRACTION___'; private $comments; private $preserved_tokens; private $memory_limit; private $max_execution_time; private $pcre_backtrack_limit; private $pcre_recursion_limit; private $raise_php_limits; /** * @param bool|int $raise_php_limits * If true, PHP settings will be raised if needed */ public function __construct($raise_php_limits = TRUE) { // Set suggested PHP limits $this->memory_limit = 128 * 1048576; // 128MB in bytes $this->max_execution_time = 60; // 1 min $this->pcre_backtrack_limit = 1000 * 1000; $this->pcre_recursion_limit = 500 * 1000; $this->raise_php_limits = (bool) $raise_php_limits; } /** * Minify a string of CSS * @param string $css * @param int|bool $linebreak_pos * @return string */ public function run($css = '', $linebreak_pos = FALSE) { if (empty($css)) { return ''; } if ($this->raise_php_limits) { $this->do_raise_php_limits(); } $this->comments = array(); $this->preserved_tokens = array(); $start_index = 0; $length = strlen($css); $css = $this->extract_data_urls($css); // collect all comment blocks... while (($start_index = $this->index_of($css, '/*', $start_index)) >= 0) { $end_index = $this->index_of($css, '*/', $start_index + 2); if ($end_index < 0) { $end_index = $length; } $comment_found = $this->str_slice($css, $start_index + 2, $end_index); $this->comments[] = $comment_found; $comment_preserve_string = self::COMMENT . (count($this->comments) - 1) . '___'; $css = $this->str_slice($css, 0, $start_index + 2) . $comment_preserve_string . $this->str_slice($css, $end_index); // Set correct start_index: Fixes issue #2528130 $start_index = $end_index + 2 + strlen($comment_preserve_string) - strlen($comment_found); } // preserve strings so their content doesn't get accidentally minified $css = preg_replace_callback('/(?:"(?:[^\\\\"]|\\\\.|\\\\)*")|'."(?:'(?:[^\\\\']|\\\\.|\\\\)*')/S", array($this, 'replace_string'), $css); // Let's divide css code in chunks of 5.000 chars aprox. // Reason: PHP's PCRE functions like preg_replace have a "backtrack limit" // of 100.000 chars by default (php < 5.3.7) so if we're dealing with really // long strings and a (sub)pattern matches a number of chars greater than // the backtrack limit number (i.e. /(.*)/s) PCRE functions may fail silently // returning NULL and $css would be empty. $charset = ''; $charset_regexp = '/(@charset)( [^;]+;)/i'; $css_chunks = array(); $css_chunk_length = 5000; // aprox size, not exact $start_index = 0; $i = $css_chunk_length; // save initial iterations $l = strlen($css); // if the number of characters is 5000 or less, do not chunk if ($l <= $css_chunk_length) { $css_chunks[] = $css; } else { // chunk css code securely while ($i < $l) { $i += 50; // save iterations if ($l - $start_index <= $css_chunk_length || $i >= $l) { $css_chunks[] = $this->str_slice($css, $start_index); break; } if ($css[$i - 1] === '}' && $i - $start_index > $css_chunk_length) { // If there are two ending curly braces }} separated or not by spaces, // join them in the same chunk (i.e. @media blocks) $next_chunk = substr($css, $i); if (preg_match('/^\s*\}/', $next_chunk)) { $i = $i + $this->index_of($next_chunk, '}') + 1; } $css_chunks[] = $this->str_slice($css, $start_index, $i); $start_index = $i; } } } // Minify each chunk for ($i = 0, $n = count($css_chunks); $i < $n; $i++) { $css_chunks[$i] = $this->minify($css_chunks[$i], $linebreak_pos); // Keep the first @charset at-rule found if (empty($charset) && preg_match($charset_regexp, $css_chunks[$i], $matches)) { $charset = strtolower($matches[1]) . $matches[2]; } // Delete all @charset at-rules $css_chunks[$i] = preg_replace($charset_regexp, '', $css_chunks[$i]); } // Update the first chunk and push the charset to the top of the file. $css_chunks[0] = $charset . $css_chunks[0]; return implode('', $css_chunks); } /** * Sets the memory limit for this script * @param int|string $limit */ public function set_memory_limit($limit) { $this->memory_limit = $this->normalize_int($limit); } /** * Sets the maximum execution time for this script * @param int|string $seconds */ public function set_max_execution_time($seconds) { $this->max_execution_time = (int) $seconds; } /** * Sets the PCRE backtrack limit for this script * @param int $limit */ public function set_pcre_backtrack_limit($limit) { $this->pcre_backtrack_limit = (int) $limit; } /** * Sets the PCRE recursion limit for this script * @param int $limit */ public function set_pcre_recursion_limit($limit) { $this->pcre_recursion_limit = (int) $limit; } /** * Try to configure PHP to use at least the suggested minimum settings */ private function do_raise_php_limits() { $php_limits = array( 'memory_limit' => $this->memory_limit, 'max_execution_time' => $this->max_execution_time, 'pcre.backtrack_limit' => $this->pcre_backtrack_limit, 'pcre.recursion_limit' => $this->pcre_recursion_limit ); // If current settings are higher respect them. foreach ($php_limits as $name => $suggested) { $current = $this->normalize_int(ini_get($name)); // memory_limit exception: allow -1 for "no memory limit". if ($current > -1 && ($suggested == -1 || $current < $suggested)) { ini_set($name, $suggested); } } } /** * Does bulk of the minification * @param string $css * @param int|bool $linebreak_pos * @return string */ private function minify($css, $linebreak_pos) { // strings are safe, now wrestle the comments for ($i = 0, $max = count($this->comments); $i < $max; $i++) { $token = $this->comments[$i]; $placeholder = '/' . self::COMMENT . $i . '___/'; // ! in the first position of the comment means preserve // so push to the preserved tokens keeping the ! if (substr($token, 0, 1) === '!') { $this->preserved_tokens[] = $token; $token_tring = self::TOKEN . (count($this->preserved_tokens) - 1) . '___'; $css = preg_replace($placeholder, $token_tring, $css, 1); // Preserve new lines for /*! important comments $css = preg_replace('/\s*[\n\r\f]+\s*(\/\*'. $token_tring .')/S', self::NL.'$1', $css); $css = preg_replace('/('. $token_tring .'\*\/)\s*[\n\r\f]+\s*/', '$1'.self::NL, $css); continue; } // \ in the last position looks like hack for Mac/IE5 // shorten that to /*\*/ and the next one to /**/ if (substr($token, (strlen($token) - 1), 1) === '\\') { $this->preserved_tokens[] = '\\'; $css = preg_replace($placeholder, self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1); $i = $i + 1; // attn: advancing the loop $this->preserved_tokens[] = ''; $css = preg_replace('/' . self::COMMENT . $i . '___/', self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1); continue; } // keep empty comments after child selectors (IE7 hack) // e.g. html >/**/ body if (strlen($token) === 0) { $start_index = $this->index_of($css, $this->str_slice($placeholder, 1, -1)); if ($start_index > 2) { if (substr($css, $start_index - 3, 1) === '>') { $this->preserved_tokens[] = ''; $css = preg_replace($placeholder, self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1); } } } // in all other cases kill the comment $css = preg_replace('/\/\*' . $this->str_slice($placeholder, 1, -1) . '\*\//', '', $css, 1); } // Normalize all whitespace strings to single spaces. Easier to work with that way. $css = preg_replace('/\s+/', ' ', $css); // preserve flex, keeping percentage even if 0 $css = preg_replace_callback('/flex\s?:\s?((?:[0-9 ]*)\s?(?:px|em|auto|%)?(?:calc\(.*\))?)/i',array($this, 'replace_flex'),$css); // Fix IE7 issue on matrix filters which browser accept whitespaces between Matrix parameters $css = preg_replace_callback('/\s*filter\:\s*progid:DXImageTransform\.Microsoft\.Matrix\(([^\)]+)\)/', array($this, 'preserve_old_IE_specific_matrix_definition'), $css); // Shorten & preserve calculations calc(...) since spaces are important $css = preg_replace_callback('/calc(\(((?:[^\(\)]+|(?1))*)\))/i', array($this, 'replace_calc'), $css); // Replace positive sign from numbers preceded by : or a white-space before the leading space is removed // +1.2em to 1.2em, +.8px to .8px, +2% to 2% $css = preg_replace('/((? -9.0 to -9 $css = preg_replace('/((?\+\(\)\]\~\=,])/', '$1', $css); // Restore spaces for !important $css = preg_replace('/\!important/i', ' !important', $css); // bring back the colon $css = preg_replace('/' . self::CLASSCOLON . '/', ':', $css); // retain space for special IE6 cases $css = preg_replace_callback('/\:first\-(line|letter)(\{|,)/i', array($this, 'lowercase_pseudo_first'), $css); // no space after the end of a preserved comment $css = preg_replace('/\*\/ /', '*/', $css); // lowercase some popular @directives $css = preg_replace_callback('/@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/i', array($this, 'lowercase_directives'), $css); // lowercase some more common pseudo-elements $css = preg_replace_callback('/:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/i', array($this, 'lowercase_pseudo_elements'), $css); // lowercase some more common functions $css = preg_replace_callback('/:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\(/i', array($this, 'lowercase_common_functions'), $css); // lower case some common function that can be values // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us $css = preg_replace_callback('/([:,\( ]\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)/iS', array($this, 'lowercase_common_functions_values'), $css); // Put the space back in some cases, to support stuff like // @media screen and (-webkit-min-device-pixel-ratio:0){ $css = preg_replace('/\band\(/i', 'and (', $css); // Remove the spaces after the things that should not have spaces after them. $css = preg_replace('/([\!\{\}\:;\>\+\(\[\~\=,])\s+/S', '$1', $css); // remove unnecessary semicolons $css = preg_replace('/;+\}/', '}', $css); // Fix for issue: #2528146 // Restore semicolon if the last property is prefixed with a `*` (lte IE7 hack) // to avoid issues on Symbian S60 3.x browsers. $css = preg_replace('/(\*[a-z0-9\-]+\s*\:[^;\}]+)(\})/', '$1;$2', $css); // Replace 0 and 0 values with 0. // data type: https://developer.mozilla.org/en-US/docs/Web/CSS/length // data type: https://developer.mozilla.org/en-US/docs/Web/CSS/percentage $css = preg_replace('/([^\\\\]\:|\s)0(?:em|ex|ch|rem|vw|vh|vm|vmin|cm|mm|in|px|pt|pc|%)/iS', '${1}0', $css); // 0% step in a keyframe? restore the % unit $css = preg_replace_callback('/(@[a-z\-]*?keyframes[^\{]+\{)(.*?)(\}\})/iS', array($this, 'replace_keyframe_zero'), $css); // Replace 0 0; or 0 0 0; or 0 0 0 0; with 0. $css = preg_replace('/\:0(?: 0){1,3}(;|\}| \!)/', ':0$1', $css); // Fix for issue: #2528142 // Replace text-shadow:0; with text-shadow:0 0 0; $css = preg_replace('/(text-shadow\:0)(;|\}| \!)/i', '$1 0 0$2', $css); // Replace background-position:0; with background-position:0 0; // same for transform-origin // Changing -webkit-mask-position: 0 0 to just a single 0 will result in the second parameter defaulting to 50% (center) $css = preg_replace('/(background|background\-position|webkit-mask-position|(?:webkit|moz|o|ms|)\-?transform\-origin)\:0(;|\}| \!)/iS', '$1:0 0$2', $css); // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space) // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space) // This makes it more likely that it'll get further compressed in the next step. $css = preg_replace_callback('/rgb\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'rgb_to_hex'), $css); $css = preg_replace_callback('/hsl\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'hsl_to_hex'), $css); // Shorten colors from #AABBCC to #ABC or short color name. $css = $this->compress_hex_colors($css); // border: none to border:0, outline: none to outline:0 $css = preg_replace('/(border\-?(?:top|right|bottom|left|)|outline)\:none(;|\}| \!)/iS', '$1:0$2', $css); // shorter opacity IE filter $css = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $css); // Find a fraction that is used for Opera's -o-device-pixel-ratio query // Add token to add the "\" back in later $css = preg_replace('/\(([a-z\-]+):([0-9]+)\/([0-9]+)\)/i', '($1:$2'. self::QUERY_FRACTION .'$3)', $css); // Remove empty rules. $css = preg_replace('/[^\};\{\/]+\{\}/S', '', $css); // Add "/" back to fix Opera -o-device-pixel-ratio query $css = preg_replace('/'. self::QUERY_FRACTION .'/', '/', $css); // Replace multiple semi-colons in a row by a single one // See SF bug #1980989 $css = preg_replace('/;;+/', ';', $css); // Restore new lines for /*! important comments $css = preg_replace('/'. self::NL .'/', "\n", $css); // Lowercase all uppercase properties $css = preg_replace_callback('/(\{|\;)([A-Z\-]+)(\:)/', array($this, 'lowercase_properties'), $css); // Some source control tools don't like it when files containing lines longer // than, say 8000 characters, are checked in. The linebreak option is used in // that case to split long lines after a specific column. if ($linebreak_pos !== FALSE && (int) $linebreak_pos >= 0) { $linebreak_pos = (int) $linebreak_pos; $start_index = $i = 0; while ($i < strlen($css)) { $i++; if ($css[$i - 1] === '}' && $i - $start_index > $linebreak_pos) { $css = $this->str_slice($css, 0, $i) . "\n" . $this->str_slice($css, $i); $start_index = $i; } } } // restore preserved comments and strings in reverse order for ($i = count($this->preserved_tokens) - 1; $i >= 0; $i--) { $css = preg_replace('/' . self::TOKEN . $i . '___/', $this->preserved_tokens[$i], $css, 1); // $css.=$this->preserved_tokens[$i]; } // Trim the final string (for any leading or trailing white spaces) return trim($css); } /** * Utility method to replace all data urls with tokens before we start * compressing, to avoid performance issues running some of the subsequent * regexes against large strings chunks. * * @param string $css * @return string */ private function extract_data_urls($css) { // Leave data urls alone to increase parse performance. $max_index = strlen($css) - 1; $append_index = $index = $last_index = $offset = 0; $sb = array(); $pattern = '/url\(\s*(["\']?)data\:/i'; // Since we need to account for non-base64 data urls, we need to handle // ' and ) being part of the data string. Hence switching to indexOf, // to determine whether or not we have matching string terminators and // handling sb appends directly, instead of using matcher.append* methods. while (preg_match($pattern, $css, $m, 0, $offset)) { $index = $this->index_of($css, $m[0], $offset); $last_index = $index + strlen($m[0]); $start_index = $index + 4; // "url(".length() $end_index = $last_index - 1; $terminator = $m[1]; // ', " or empty (not quoted) $found_terminator = FALSE; if (strlen($terminator) === 0) { $terminator = ')'; } while ($found_terminator === FALSE && $end_index+1 <= $max_index) { $end_index = $this->index_of($css, $terminator, $end_index + 1); // endIndex == 0 doesn't really apply here if ($end_index > 0 && substr($css, $end_index - 1, 1) !== '\\') { $found_terminator = TRUE; if (')' != $terminator) { $end_index = $this->index_of($css, ')', $end_index); } } } // Enough searching, start moving stuff over to the buffer $sb[] = $this->str_slice($css, $append_index, $index); if ($found_terminator) { $token = $this->str_slice($css, $start_index, $end_index); if (strpos($token,"preserved_tokens[] = $token; $preserver = 'url(' . self::TOKEN . (count($this->preserved_tokens) - 1) . '___)'; $sb[] = $preserver; $append_index = $end_index + 1; } else { // No end terminator found, re-add the whole match. Should we throw/warn here? $sb[] = $this->str_slice($css, $index, $last_index); $append_index = $last_index; } $offset = $last_index; } $sb[] = $this->str_slice($css, $append_index); return implode('', $sb); } /** * Utility method to compress hex color values of the form #AABBCC to #ABC or short color name. * * DOES NOT compress CSS ID selectors which match the above pattern (which would break things). * e.g. #AddressForm { ... } * * DOES NOT compress IE filters, which have hex color values (which would break things). * e.g. filter: chroma(color="#FFFFFF"); * * DOES NOT compress invalid hex values. * e.g. background-color: #aabbccdd * * @param string $css * @return string */ private function compress_hex_colors($css) { // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters) $pattern = '/(\=\s*?["\']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/iS'; $_index = $index = $last_index = $offset = 0; $sb = array(); // See: http://ajaxmin.codeplex.com/wikipage?title=CSS%20Colors $short_safe = array( '#808080' => 'gray', '#008000' => 'green', '#800000' => 'maroon', '#000080' => 'navy', '#808000' => 'olive', '#ffa500' => 'orange', '#800080' => 'purple', '#c0c0c0' => 'silver', '#008080' => 'teal', '#f00' => 'red' ); while (preg_match($pattern, $css, $m, 0, $offset)) { $index = $this->index_of($css, $m[0], $offset); $last_index = $index + strlen($m[0]); $is_filter = $m[1] !== null && $m[1] !== ''; $sb[] = $this->str_slice($css, $_index, $index); if ($is_filter) { // Restore, maintain case, otherwise filter will break $sb[] = $m[1] . '#' . $m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]; } else { if (strtolower($m[2]) == strtolower($m[3]) && strtolower($m[4]) == strtolower($m[5]) && strtolower($m[6]) == strtolower($m[7])) { // Compress. $hex = '#' . strtolower($m[3] . $m[5] . $m[7]); } else { // Non compressible color, restore but lower case. $hex = '#' . strtolower($m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]); } // replace Hex colors to short safe color names $sb[] = array_key_exists($hex, $short_safe) ? $short_safe[$hex] : $hex; } $_index = $offset = $last_index - strlen($m[8]); } $sb[] = $this->str_slice($css, $_index); return implode('', $sb); } /* CALLBACKS * --------------------------------------------------------------------------------------------- */ private function replace_string($matches) { $match = $matches[0]; $quote = substr($match, 0, 1); // Must use addcslashes in PHP to avoid parsing of backslashes $match = addcslashes($this->str_slice($match, 1, -1), '\\'); // maybe the string contains a comment-like substring? // one, maybe more? put'em back then if (($pos = $this->index_of($match, self::COMMENT)) >= 0) { for ($i = 0, $max = count($this->comments); $i < $max; $i++) { $match = preg_replace('/' . self::COMMENT . $i . '___/', $this->comments[$i], $match, 1); } } // minify alpha opacity in filter strings $match = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $match); $this->preserved_tokens[] = $match; return $quote . self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . $quote; } private function replace_colon($matches) { return preg_replace('/\:/', self::CLASSCOLON, $matches[0]); } private function replace_calc($matches) { $this->preserved_tokens[] = preg_replace('/([\+\-]{1})\(/','$1 (',trim(preg_replace('/\s*([\*\/\(\),])\s*/', '$1', $matches[2]))); return 'calc('. self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . ')'; } private function replace_flex($matches) { $this->preserved_tokens[] = trim($matches[1]); return 'flex:'.self::TOKEN . (count($this->preserved_tokens) - 1) . '___'; } private function preserve_old_IE_specific_matrix_definition($matches) { $this->preserved_tokens[] = $matches[1]; return 'filter:progid:DXImageTransform.Microsoft.Matrix(' . self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . ')'; } private function replace_keyframe_zero($matches) { return $matches[1] . preg_replace('/0(\{|,[^\)\{]+\{)/', '0%$1', $matches[2]) . $matches[3]; } private function rgb_to_hex($matches) { // Support for percentage values rgb(100%, 0%, 45%); if ($this->index_of($matches[1], '%') >= 0){ $rgbcolors = explode(',', str_replace('%', '', $matches[1])); for ($i = 0; $i < count($rgbcolors); $i++) { $rgbcolors[$i] = $this->round_number(floatval($rgbcolors[$i]) * 2.55); } } else { $rgbcolors = explode(',', $matches[1]); } // Values outside the sRGB color space should be clipped (0-255) for ($i = 0; $i < count($rgbcolors); $i++) { $rgbcolors[$i] = $this->clamp_number(intval($rgbcolors[$i], 10), 0, 255); $rgbcolors[$i] = sprintf("%02x", $rgbcolors[$i]); } // Fix for issue #2528093 if (!preg_match('/[\s\,\);\}]/', $matches[2])){ $matches[2] = ' ' . $matches[2]; } return '#' . implode('', $rgbcolors) . $matches[2]; } private function hsl_to_hex($matches) { $values = explode(',', str_replace('%', '', $matches[1])); $h = floatval($values[0]); $s = floatval($values[1]); $l = floatval($values[2]); // Wrap and clamp, then fraction! $h = ((($h % 360) + 360) % 360) / 360; $s = $this->clamp_number($s, 0, 100) / 100; $l = $this->clamp_number($l, 0, 100) / 100; if ($s == 0) { $r = $g = $b = $this->round_number(255 * $l); } else { $v2 = $l < 0.5 ? $l * (1 + $s) : ($l + $s) - ($s * $l); $v1 = (2 * $l) - $v2; $r = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h + (1/3))); $g = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h)); $b = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h - (1/3))); } return $this->rgb_to_hex(array('', $r.','.$g.','.$b, $matches[2])); } private function lowercase_pseudo_first($matches) { return ':first-'. strtolower($matches[1]) .' '. $matches[2]; } private function lowercase_directives($matches) { return '@'. strtolower($matches[1]); } private function lowercase_pseudo_elements($matches) { return ':'. strtolower($matches[1]); } private function lowercase_common_functions($matches) { return ':'. strtolower($matches[1]) .'('; } private function lowercase_common_functions_values($matches) { return $matches[1] . strtolower($matches[2]); } private function lowercase_properties($matches) { return $matches[1].strtolower($matches[2]).$matches[3]; } /* HELPERS * --------------------------------------------------------------------------------------------- */ private function hue_to_rgb($v1, $v2, $vh) { $vh = $vh < 0 ? $vh + 1 : ($vh > 1 ? $vh - 1 : $vh); if ($vh * 6 < 1) return $v1 + ($v2 - $v1) * 6 * $vh; if ($vh * 2 < 1) return $v2; if ($vh * 3 < 2) return $v1 + ($v2 - $v1) * ((2/3) - $vh) * 6; return $v1; } private function round_number($n) { return intval(floor(floatval($n) + 0.5), 10); } private function clamp_number($n, $min, $max) { return min(max($n, $min), $max); } /** * PHP port of Javascript's "indexOf" function for strings only * Author: Tubal Martin http://blog.margenn.com * * @param string $haystack * @param string $needle * @param int $offset index (optional) * @return int */ private function index_of($haystack, $needle, $offset = 0) { $index = strpos($haystack, $needle, $offset); return ($index !== FALSE) ? $index : -1; } /** * PHP port of Javascript's "slice" function for strings only * Author: Tubal Martin http://blog.margenn.com * Tests: http://margenn.com/tubal/str_slice/ * * @param string $str * @param int $start index * @param int|bool $end index (optional) * @return string */ private function str_slice($str, $start = 0, $end = FALSE) { if ($end !== FALSE && ($start < 0 || $end <= 0)) { $max = strlen($str); if ($start < 0) { if (($start = $max + $start) < 0) { return ''; } } if ($end < 0) { if (($end = $max + $end) < 0) { return ''; } } if ($end <= $start) { return ''; } } $slice = ($end === FALSE) ? substr($str, $start) : substr($str, $start, $end - $start); return ($slice === FALSE) ? '' : $slice; } /** * Convert strings like "64M" or "30" to int values * @param mixed $size * @return int */ private function normalize_int($size) { if (is_string($size)) { switch (substr($size, -1)) { case 'M': case 'm': return $size * 1048576; case 'K': case 'k': return $size * 1024; case 'G': case 'g': return $size * 1073741824; } } return (int) $size; } } Hungarian recurve bows for sale – Junze™ Archery
info@archerysupplier.com
+86-186-6635-5451

Hungarian recurve bows for sale

Looking for an authentic handmade recurve bows for sale ? The Junze® Archery 20-110 LB Hungarian recurve bow is a handmade bow. This multifunctional bow can be used for a various hunting skills. Let’s take a closer look at the longbow.
Longbowmaker Hungarian Recurve Longbow Review

  • Draw weight of 30-80lbs
  • The recurve bow is made out of wood, cow leather, glass steel, beech
  • Its draw length is 28 inches with a safety draw length of 33 inches
  • 145cm in body length with 131cm in string.
  • Extremely light weight
  • Uses longbow arrows

Pros

Great for beginners
The great thing about the Recurve bows is that if you’re looking to develop your skill as a shooter, you are not given any assistance through its features. This is a traditional bow where you’ll be using wood arrows and learning the true essentials of crossbow hunting and target shooting. So, if you’re looking for a crossbow that will develop your foundation as a shooter, this is a great bow for that.

Handmade
This may not be a crucial pro, but it’s always a nice feature to have a hand-crafted bow. By it being handmade, you can ensure a high-quality crossbow. In addition, each bow purchased is tested prior to shipment, double checking the product before it reaches the customers hands. That, to me, is quality and consideration for their craft and the hunter.

Affordable
What can I say, it’s a great thing not having to break the bank to enjoy hunting or target shooting. For under $150, you can have a beautifully hand-crafted bow. Especially if you’re a beginner, having it at an affordable price will put you at some financial ease.

Cons

Cheap bowstring
Though the longbow itself is very durable and high-quality, the free bowstring that comes with it is not as durable as expected. You will need to invest in a higher quality bowstring for your traditional recurve bow.

Frequently Asked Questions

Longbowmaker Hungarian Recurve Longbow Review
Does the Hungarian recurve bow come with arrows?
No, the bow doesn’t come with arrows. You will have to buy the arrows separately.

What accessories does the recurve bow come with?
This longbow comes with a soft storage bag and bowstring.

What size of arrows should I buy for this bow?
The average draw is 28”, so any 32” arrows will work for this bow if you have an average arm length.

What is the best type of arrow to use for this bow?
You can use wood or bamboo arrows – it’s based on preference.

Does this bow work for left-handed users?
Yes, this bow is ambidextrous, so it is suitable for both left and right handed archers.

Conclusion

Longbowmaker Hungarian Recurve Longbow Review
This recurve bow is truly a classic piece. The fact that it is a handmade longbow is a rare find, as many crossbows today are factory made. The handmade recurve bow is extremely light weight, sturdy and beautifully designed. This longbow will really help you in developing your skills as a hunter by increasing your accuracy, power and speed. A great beginner bow or if you’re looking for more of a challenge as a hunter, this is also very suitable for you. You will need to buy a new bowstring for this Hungarian recurve bow, however, if you can handle that then this longbow will not let you down on the open terrain.

Related Article

The most cost-effective recurve bows for sale on the market

Wooden Recurve Bows For Sale