616 lines
18 KiB
Text
616 lines
18 KiB
Text
<?php
|
|
|
|
/**
|
|
* @file
|
|
* Advanced CSS/JS aggregation js compression module.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Default value to see if the callback is working.
|
|
*/
|
|
define('ADVAGG_JS_COMPRESS_CALLBACK', FALSE);
|
|
|
|
/**
|
|
* Default value to see packer is enabled.
|
|
*/
|
|
define('ADVAGG_JS_COMPRESS_PACKER_ENABLE', FALSE);
|
|
|
|
/**
|
|
* Default value to see what compressor to use. 0 is JSMin+.
|
|
*/
|
|
define('ADVAGG_JS_COMPRESSOR', 0);
|
|
|
|
/**
|
|
* Default value for the compression ratio test.
|
|
*/
|
|
define('ADVAGG_JS_COMPRESS_RATIO', 0.1);
|
|
|
|
/**
|
|
* Default value for the compression ratio test.
|
|
*/
|
|
define('ADVAGG_JS_MAX_COMPRESS_RATIO', 0.98);
|
|
|
|
/**
|
|
* Default value to see if this will compress aggregated files.
|
|
*/
|
|
define('ADVAGG_JS_COMPRESS_AGG_FILES', TRUE);
|
|
|
|
/**
|
|
* Default value to see if this will compress inline js.
|
|
*/
|
|
define('ADVAGG_JS_COMPRESS_INLINE', TRUE);
|
|
|
|
/**
|
|
* Default value to see if this will cache the compressed inline js.
|
|
*/
|
|
define('ADVAGG_JS_COMPRESS_INLINE_CACHE', TRUE);
|
|
|
|
/**
|
|
* Default value to see if this will cache the compressed inline js.
|
|
*/
|
|
define('ADVAGG_JS_COMPRESS_FILE_CACHE', TRUE);
|
|
|
|
/**
|
|
* Implementation of hook_menu
|
|
*/
|
|
function advagg_js_compress_menu() {
|
|
$items = array();
|
|
$file_path = drupal_get_path('module', 'advagg_js_compress');
|
|
|
|
$items['advagg/js_compress_test_file'] = array(
|
|
'page callback' => 'advagg_js_compress_test_file',
|
|
'type' => MENU_CALLBACK,
|
|
'access callback' => TRUE,
|
|
);
|
|
$items['admin/settings/advagg/js-compress'] = array(
|
|
'title' => 'JS Compression',
|
|
'description' => 'Adjust JS Compression settings.',
|
|
'page callback' => 'advagg_js_compress_admin_page',
|
|
'type' => MENU_LOCAL_TASK,
|
|
'access arguments' => array('administer site configuration'),
|
|
'file path' => $file_path,
|
|
'file' => 'advagg_js_compress.admin.inc',
|
|
'weight' => 10,
|
|
);
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Implement hook_init.
|
|
*/
|
|
function advagg_js_compress_init() {
|
|
global $conf;
|
|
|
|
if (variable_get('advagg_js_compress_packer_enable', ADVAGG_JS_COMPRESS_PACKER_ENABLE)) {
|
|
$conf['advagg_file_save_function'] = 'advagg_js_compress_file_saver';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implement hook_advagg_files_table.
|
|
*/
|
|
function advagg_js_compress_advagg_files_table($row, $checksum) {
|
|
// IF the file has changed, test it's compressibility.
|
|
if ($row['filetype'] === 'js' && $checksum !== $row['checksum']) {
|
|
$files_to_test[] = array(
|
|
'md5' => $row['filename_md5'],
|
|
'filename' => $row['filename'],
|
|
);
|
|
advagg_js_compress_test_compression($files_to_test);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implement hook_advagg_js_pre_alter.
|
|
*/
|
|
function advagg_js_compress_advagg_js_pre_alter(&$javascript, $preprocess_js, $public_downloads, $scope) {
|
|
if (module_exists('jquery_update')) {
|
|
return;
|
|
}
|
|
|
|
foreach ($javascript as $type => $data) {
|
|
if (!$data) {
|
|
continue;
|
|
}
|
|
if ($type == 'setting' || $type == 'inline') {
|
|
continue;
|
|
}
|
|
foreach ($data as $path => $info) {
|
|
if ($path == 'misc/jquery.form.js') {
|
|
$new_path = drupal_get_path('module', 'advagg_js_compress') . '/jquery.form.js';
|
|
$javascript[$type][$new_path] = $info;
|
|
unset($javascript[$type][$path]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implement hook_advagg_js_alter.
|
|
*/
|
|
function advagg_js_compress_advagg_js_alter(&$contents, $files, $bundle_md5) {
|
|
if (!variable_get('advagg_js_compress_agg_files', ADVAGG_JS_COMPRESS_AGG_FILES)) {
|
|
return;
|
|
}
|
|
|
|
advagg_js_compress_prep($contents, $files, $bundle_md5);
|
|
}
|
|
|
|
/**
|
|
* Implement hook_advagg_js_inline_alter.
|
|
*/
|
|
function advagg_js_compress_advagg_js_inline_alter(&$contents) {
|
|
if (!variable_get('advagg_js_compress_inline', ADVAGG_JS_COMPRESS_INLINE)) {
|
|
return;
|
|
}
|
|
$compressor = variable_get('advagg_js_compressor', ADVAGG_JS_COMPRESSOR);
|
|
|
|
// If using a cache, try to get the contents of it.
|
|
if (variable_get('advagg_js_compress_inline_cache', ADVAGG_JS_COMPRESS_INLINE_CACHE)) {
|
|
$key = md5($contents) . $compressor;
|
|
$table = 'cache_advagg_js_compress_inline';
|
|
$data = cache_get($key, $table);
|
|
if (!empty($data->data)) {
|
|
$contents = $data->data;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ($compressor == 0) {
|
|
$original_contents = $contents;
|
|
|
|
list($before, $after) = advagg_js_compress_jsminplus($contents);
|
|
$ratio = 0;
|
|
if ($before != 0) {
|
|
$ratio = ($before - $after) / $before;
|
|
}
|
|
// Make sure the returned string is not empty or has a VERY high
|
|
// compression ratio.
|
|
if (empty($contents) || empty($ratio) || $ratio > variable_get('advagg_js_max_compress_ratio', ADVAGG_JS_MAX_COMPRESS_RATIO)) {
|
|
$contents = $original_contents;
|
|
}
|
|
|
|
}
|
|
if ($compressor == 1) {
|
|
$contents = jsmin($contents);
|
|
|
|
// Ensure that $contents ends with ; or }.
|
|
if (strpbrk(substr(trim($contents), -1), ';}') === FALSE) {
|
|
// ; or } not found, add in ; to the end of $contents.
|
|
$contents = trim($contents) . ';';
|
|
}
|
|
}
|
|
|
|
// If using a cache set it.
|
|
if (isset($key)) {
|
|
cache_set($key, $contents, $table, CACHE_TEMPORARY);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compress a JS string
|
|
*
|
|
* @param $contents
|
|
* Javascript string.
|
|
*/
|
|
function advagg_js_compress_prep(&$contents, $files, $bundle_md5) {
|
|
// Make sure every file in this aggregate is compressible.
|
|
$files_to_test = array();
|
|
$list_bad = array();
|
|
foreach ($files as $filename) {
|
|
$filename_md5 = md5($filename);
|
|
$data = advagg_get_file_data($filename_md5);
|
|
|
|
// File needs to be tested.
|
|
if (empty($data['advagg_js_compress']['tested'])) {
|
|
$files_to_test[] = array(
|
|
'md5' => $filename_md5,
|
|
'filename' => $filename,
|
|
);
|
|
}
|
|
elseif ($data['advagg_js_compress']['tested']['jsminplus'] != 1) {
|
|
$list_bad[$filename] = $filename;
|
|
}
|
|
}
|
|
|
|
$advagg_js_compress_callback = variable_get('advagg_js_compress_callback', ADVAGG_JS_COMPRESS_CALLBACK);
|
|
if ($advagg_js_compress_callback) {
|
|
// Send test files to worker.
|
|
if (!empty($files_to_test)) {
|
|
$compressible = advagg_js_compress_test_compression($files_to_test);
|
|
// If an array then it is a list of files that can not be compressed.
|
|
if (is_array($compressible)) {
|
|
// Place filename in an array key.
|
|
foreach ($compressible as $filedata) {
|
|
$filename = $filedata['filename'];
|
|
$list_bad[$filename] = $filename;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$contents = '';
|
|
// Do not compress the file that it bombs on.
|
|
// Compress each file individually.
|
|
foreach ($files as $file) {
|
|
if (!empty($list_bad[$file])) {
|
|
$contents .= advagg_build_js_bundle(array($file));
|
|
}
|
|
else {
|
|
$data = advagg_build_js_bundle(array($file));
|
|
|
|
// If using a cache, try to get the contents of it.
|
|
$cached = FALSE;
|
|
if (variable_get('advagg_js_compress_file_cache', ADVAGG_JS_COMPRESS_FILE_CACHE)) {
|
|
$key = $file;
|
|
$table = 'cache_advagg_js_compress_file';
|
|
$cached_data = cache_get($key, $table);
|
|
if (!empty($cached_data->data)) {
|
|
$data = $cached_data->data;
|
|
$cached = TRUE;
|
|
}
|
|
}
|
|
if (!$cached && !empty($data)) {
|
|
$compressor = variable_get('advagg_js_compressor', ADVAGG_JS_COMPRESSOR);
|
|
if ($compressor == 0) {
|
|
list($before, $after) = advagg_js_compress_jsminplus($data);
|
|
$ratio = 0;
|
|
if ($before != 0) {
|
|
$ratio = ($before - $after) / $before;
|
|
}
|
|
// Make sure the returned string is not empty or has a VERY high
|
|
// compression ratio.
|
|
if (empty($data) || empty($ratio) || $ratio > variable_get('advagg_js_max_compress_ratio', ADVAGG_JS_MAX_COMPRESS_RATIO)) {
|
|
$data = advagg_build_js_bundle(array($file));
|
|
}
|
|
elseif (isset($key)) {
|
|
// If using a cache set it.
|
|
cache_set($key, $data, $table);
|
|
}
|
|
}
|
|
elseif ($compressor == 1) {
|
|
$contents = jsmin($contents);
|
|
|
|
// Ensure that $contents ends with ; or }.
|
|
if (strpbrk(substr(trim($contents), -1), ';}') === FALSE) {
|
|
// ; or } not found, add in ; to the end of $contents.
|
|
$contents = trim($contents) . ';';
|
|
}
|
|
}
|
|
}
|
|
$url = url($file, array('absolute' => TRUE));
|
|
$contents .= "/* Source and licensing information for the line(s) below can be found at $url. */\n" . $data . ";\n/* Source and licensing information for the above line(s) can be found at $url. */\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compress a JS string using jsmin+
|
|
*
|
|
* @param $contents
|
|
* Javascript string.
|
|
* @return
|
|
* array with the size before and after.
|
|
*/
|
|
function advagg_js_compress_jsminplus(&$contents) {
|
|
// Try to allocate enough time to run JSMin+.
|
|
if (function_exists('set_time_limit')) {
|
|
@set_time_limit(variable_get('advagg_set_time_limit', ADVAGG_SET_TIME_LIMIT));
|
|
}
|
|
|
|
// Only include jsminplus.inc if the JSMinPlus class doesn't exist.
|
|
if (!class_exists('JSMinPlus')) {
|
|
include(drupal_get_path('module', 'advagg_js_compress') . '/jsminplus.inc');
|
|
}
|
|
// Get the JS string length before the compression operation.
|
|
$before = strlen($contents);
|
|
$original_contents = $contents;
|
|
try {
|
|
// Strip Byte Order Marks (BOM's) from the file, JSMin+ cannot parse these.
|
|
$contents = str_replace(pack("CCC", 0xef, 0xbb, 0xbf), "", $contents);
|
|
ob_start();
|
|
// JSMin+ the contents of the aggregated file.
|
|
$contents = JSMinPlus::minify($contents);
|
|
$error = trim(ob_get_contents());
|
|
if (!empty($error)) {
|
|
throw new Exception($error);
|
|
}
|
|
|
|
// Ensure that $contents ends with ; or }.
|
|
if (strpbrk(substr(trim($contents), -1), ';}') === FALSE) {
|
|
// ; or } not found, add in ; to the end of $contents.
|
|
$contents = trim($contents) . ';';
|
|
}
|
|
|
|
// Get the JS string length after the compression operation.
|
|
$after = strlen($contents);
|
|
}
|
|
catch (Exception $e) {
|
|
// Log the exception thrown by JSMin+ and roll back to uncompressed content.
|
|
watchdog('advagg', $e->getMessage() . '<pre>' . $original_contents . '</pre>', NULL, WATCHDOG_WARNING);
|
|
$contents = $original_contents;
|
|
$after = $before;
|
|
}
|
|
ob_end_clean();
|
|
return array($before, $after);
|
|
}
|
|
|
|
/**
|
|
* Run various theme functions so the cache is primed.
|
|
*
|
|
* @param $files_to_test
|
|
* array with md5 and filename.
|
|
* @return
|
|
* TRUE if all files are compressible. List of files that failed otherwise.
|
|
*/
|
|
function advagg_js_compress_test_compression($files_to_test) {
|
|
global $base_path;
|
|
$bad_files = array();
|
|
|
|
// Blacklist jquery.min.js from getting compressed.
|
|
if (module_exists('jquery_update')) {
|
|
foreach ($files_to_test as $key => $info) {
|
|
if (strpos($info['filename'], 'jquery.min.js') !== FALSE) {
|
|
// Add file to the bad list.
|
|
$bad_files[] = $info;
|
|
unset($files_to_test[$key]);
|
|
|
|
// Get file data.
|
|
$filename_md5 = md5($info['filename']);
|
|
$lock_name = 'advagg_set_file_data_' . $filename_md5;
|
|
if (!lock_acquire($lock_name, 10)) {
|
|
lock_wait($lock_name);
|
|
continue;
|
|
}
|
|
$data = advagg_get_file_data($filename_md5);
|
|
|
|
// Set to -2
|
|
if (!isset($data->data['advagg_js_compress']['tested']['jsminplus']) || $data->data['advagg_js_compress']['tested']['jsminplus'] != -2) {
|
|
$data['advagg_js_compress']['tested']['jsminplus'] = -2;
|
|
advagg_set_file_data($filename_md5, $data);
|
|
}
|
|
lock_release($lock_name);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($files_to_test as $info) {
|
|
$key = variable_get('advagg_js_compress_url_key', FALSE);
|
|
if (empty($key)) {
|
|
$key = mt_rand();
|
|
variable_set('advagg_js_compress_url_key', $key);
|
|
}
|
|
|
|
// Clear the cache for this file
|
|
cache_clear_all($info['filename'], 'cache_advagg_js_compress_file');
|
|
|
|
// Setup request URL and headers.
|
|
$query['values'] = $info;
|
|
$query['key'] = $key;
|
|
$query_string = http_build_query($query, '', '&');
|
|
$url = _advagg_build_url('advagg/js_compress_test_file');
|
|
$headers = array(
|
|
'Host' => $_SERVER['HTTP_HOST'],
|
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
|
'Connection' => 'close',
|
|
);
|
|
|
|
$results = drupal_http_request($url, $headers, 'POST', $query_string);
|
|
|
|
// Get file data.
|
|
$filename_md5 = md5($info['filename']);
|
|
$data = advagg_get_file_data($filename_md5);
|
|
|
|
// Mark as a bad file.
|
|
if (empty($data['advagg_js_compress']['tested']['jsminplus']) || $data['advagg_js_compress']['tested']['jsminplus'] != 1) {
|
|
$bad_files[] = $info;
|
|
}
|
|
}
|
|
if (empty($bad_files)) {
|
|
return TRUE;
|
|
}
|
|
return $bad_files;
|
|
}
|
|
|
|
/**
|
|
* Run various theme functions so the cache is primed.
|
|
*
|
|
* @param $values
|
|
* object File info
|
|
*/
|
|
function advagg_js_compress_test_file($values = NULL) {
|
|
// watchdog('debug', str_replace(' ', ' ', nl2br(htmlentities(print_r($values, TRUE) . print_r($_REQUEST, TRUE)))));
|
|
|
|
// Exit if key does not match & called with $file not set.
|
|
if (is_null($values)) {
|
|
if (empty($_POST['key']) || empty($_POST['values'])) {
|
|
return;
|
|
}
|
|
$key = variable_get('advagg_js_compress_url_key', FALSE);
|
|
if ($key != $_POST['key']) {
|
|
return;
|
|
}
|
|
$values = array();
|
|
$values['values'] = $_POST['values'];
|
|
}
|
|
$filename = $values['values']['filename'];
|
|
$md5 = $values['values']['md5'];
|
|
|
|
// Compression test file if it exists.
|
|
advagg_clearstatcache(TRUE, $filename);
|
|
if (file_exists($filename)) {
|
|
$contents = file_get_contents($filename);
|
|
$filesize = filesize($filename);
|
|
|
|
$lock_name = 'advagg_set_file_data_' . $md5;
|
|
if (!lock_acquire($lock_name, 45)) {
|
|
lock_wait($lock_name);
|
|
echo $md5;
|
|
exit;
|
|
}
|
|
$data = advagg_get_file_data($md5);
|
|
|
|
// Set to "-1" so if php bombs out, the file will be marked as bad.
|
|
$data['advagg_js_compress']['tested']['jsminplus'] = -1;
|
|
advagg_set_file_data($md5, $data);
|
|
|
|
// Compress the data.
|
|
list($before, $after) = advagg_js_compress_jsminplus($contents);
|
|
|
|
// Set to "-2" if compression ratio sucks.
|
|
$ratio = 0;
|
|
if ($before != 0) {
|
|
$ratio = ($before - $after) / $before;
|
|
}
|
|
if ($ratio < variable_get('advagg_js_compress_ratio', ADVAGG_JS_COMPRESS_RATIO)) {
|
|
$data['advagg_js_compress']['tested']['jsminplus'] = -2;
|
|
advagg_set_file_data($md5, $data);
|
|
lock_release($lock_name);
|
|
echo $md5;
|
|
exit;
|
|
}
|
|
// Set to "-3" if the compression ratio is way too good.
|
|
if ($ratio > variable_get('advagg_js_max_compress_ratio', ADVAGG_JS_MAX_COMPRESS_RATIO)) {
|
|
$data['advagg_js_compress']['tested']['jsminplus'] = -3;
|
|
advagg_set_file_data($md5, $data);
|
|
lock_release($lock_name);
|
|
echo $md5;
|
|
exit;
|
|
}
|
|
|
|
// Everything worked, mark this file as compressible.
|
|
$data['advagg_js_compress']['tested']['jsminplus'] = 1;
|
|
advagg_set_file_data($md5, $data);
|
|
|
|
// Set the file cache.
|
|
if (variable_get('advagg_js_compress_file_cache', ADVAGG_JS_COMPRESS_FILE_CACHE)) {
|
|
$key = $filename;
|
|
$table = 'cache_advagg_js_compress_file';
|
|
cache_set($key, $contents, $table);
|
|
}
|
|
}
|
|
|
|
if (isset($lock_name)) {
|
|
lock_release($lock_name);
|
|
}
|
|
echo $md5;
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Save a string to the specified destination. Verify that file size is not zero.
|
|
*
|
|
* @param $data
|
|
* A string containing the contents of the file.
|
|
* @param $dest
|
|
* A string containing the destination location.
|
|
* @return
|
|
* Boolean indicating if the file save was successful.
|
|
*/
|
|
function advagg_js_compress_file_saver($data, $dest, $force, $type) {
|
|
if ($type == 'css') {
|
|
return advagg_file_saver($data, $dest, $force, $type);
|
|
}
|
|
if (!variable_get('advagg_gzip_compression', ADVAGG_GZIP_COMPRESSION) || !extension_loaded('zlib')) {
|
|
return advagg_file_saver($data, $dest, $force, $type);
|
|
}
|
|
|
|
// Get file save function
|
|
$file_save_data = 'file_save_data';
|
|
$custom_path = variable_get('advagg_custom_files_dir', ADVAGG_CUSTOM_FILES_DIR);
|
|
if (!empty($custom_path)) {
|
|
$file_save_data = 'advagg_file_save_data';
|
|
}
|
|
|
|
// Gzip first.
|
|
$gzip_dest = $dest . '.gz';
|
|
advagg_clearstatcache(TRUE, $gzip_dest);
|
|
if (!file_exists($gzip_dest) || $force) {
|
|
$gzip_data = gzencode($data, 9, FORCE_GZIP);
|
|
if (!$file_save_data($gzip_data, $gzip_dest, FILE_EXISTS_REPLACE)) {
|
|
return FALSE;
|
|
}
|
|
|
|
// Make sure filesize is not zero.
|
|
advagg_clearstatcache(TRUE, $gzip_dest);
|
|
if (@filesize($gzip_dest) == 0 && !empty($gzip_data)) {
|
|
if (!$file_save_data($gzip_data, $gzip_dest, FILE_EXISTS_REPLACE)) {
|
|
return FALSE;
|
|
}
|
|
advagg_clearstatcache(TRUE, $gzip_dest);
|
|
if (@filesize($gzip_dest) == 0 && !empty($gzip_data)) {
|
|
// Filename is bad, create a new one next time.
|
|
file_delete($gzip_dest);
|
|
return FALSE;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use packer on JS data.
|
|
advagg_js_compress_jspacker($data);
|
|
|
|
// Write File.
|
|
if (!$file_save_data($data, $dest, FILE_EXISTS_REPLACE)) {
|
|
return FALSE;
|
|
}
|
|
|
|
// Make sure filesize is not zero.
|
|
advagg_clearstatcache(TRUE, $dest);
|
|
if (@filesize($dest) == 0 && !empty($data)) {
|
|
if (!$file_save_data($data, $dest, FILE_EXISTS_REPLACE)) {
|
|
return FALSE;
|
|
}
|
|
advagg_clearstatcache(TRUE, $dest);
|
|
if (@filesize($dest) == 0 && !empty($data)) {
|
|
// Filename is bad, create a new one next time.
|
|
file_delete($dest);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
// Make sure .htaccess file exists.
|
|
advagg_htaccess_check_generate($dest);
|
|
|
|
cache_set($dest, time(), 'cache_advagg', CACHE_PERMANENT);
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Compress a JS string using packer.
|
|
*
|
|
* @param $contents
|
|
* Javascript string.
|
|
*/
|
|
function advagg_js_compress_jspacker(&$contents) {
|
|
// Use Packer on the contents of the aggregated file.
|
|
require_once(drupal_get_path('module', 'advagg_js_compress') . '/jspacker.inc');
|
|
|
|
// Add semicolons to the end of lines if missing.
|
|
$contents = str_replace("}\n", "};\n", $contents);
|
|
$contents = str_replace("\nfunction", ";\nfunction", $contents);
|
|
|
|
// Remove char returns, looking at you lightbox2.
|
|
$contents = str_replace("\n\r", "", $contents);
|
|
$contents = str_replace("\r", "", $contents);
|
|
$contents = str_replace("\n", "", $contents);
|
|
|
|
$packer = new JavaScriptPacker($contents, 62, TRUE, FALSE);
|
|
$contents = $packer->pack();
|
|
}
|
|
|
|
/**
|
|
* Implementation of hook_flush_caches().
|
|
*/
|
|
function advagg_js_compress_flush_caches() {
|
|
return array('cache_advagg_js_compress_inline');
|
|
}
|
|
|
|
/**
|
|
* Implementation of hook_advagg_master_reset().
|
|
*/
|
|
function advagg_js_compress_advagg_master_reset() {
|
|
cache_clear_all('*', 'cache_advagg_js_compress_inline', TRUE);
|
|
cache_clear_all('*', 'cache_advagg_js_compress_file', TRUE);
|
|
}
|