PluginProbe
Linguise – AI Automatic Multilingual Translation / trunk
Linguise – AI Automatic Multilingual Translation vtrunk
2.2.64 2.2.63 2.2.62 2.2.61 2.2.60 2.2.59 2.2.58 2.2.57 2.2.56 2.2.55 2.2.54 2.2.53 2.2.52 2.2.51 2.2.50 2.2.49 2.2.47 2.2.48 2.2.46 2.2.45 2.2.44 2.2.43 2.2.42 1.9.7 1.9.8 All 255 releases
linguise / src / FragmentHandler.php

FragmentHandler.php in Linguise – AI Automatic Multilingual Translation trunk, at src/FragmentHandler.php

597 lines 25.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Linguise\WordPress;
4
5 use Linguise\WordPress\FragmentBase;
6 use Linguise\WordPress\Helper;
7 use Linguise\WordPress\HTMLHelper;
8 use Linguise\Vendor\Linguise\Script\Core\Debug;
9
10 defined('ABSPATH') || die('');
11
12 require_once __DIR__ . DIRECTORY_SEPARATOR . 'FragmentOverrideHandler.php';
13 require_once __DIR__ . DIRECTORY_SEPARATOR . 'FragmentAutoHandler.php';
14 require_once __DIR__ . DIRECTORY_SEPARATOR . 'FragmentI18nHandler.php';
15
16 /**
17 * Class FragmentHandler
18 */
19 class FragmentHandler extends FragmentBase
20 {
21 /**
22 * Regex/matcher for our custom HTML fragment
23 *
24 * @var string
25 */
26 protected static $frag_html_match = '/<(div|a|linguise-main|img) class="linguise-fragment" data-fragment-name="([^"]*)" data-fragment-param="([^"]*)" data-fragment-key="([^"]*)" data-fragment-format="((?:link|html|html-main|text|media-img|media-imgset)(?:-skip)?)" data-fragment-mode="(auto|override|skip|i18n|attribute)"(?: data-fragment-extra-id="([^"]*)")?(?: (?:href|src|srcset)="([^"]*)")?(?: linguise-ignore="1")?>(.*?)<\/\1>/si';
27 /**
28 * Regex/matcher for our custom HTML fragment
29 *
30 * This version support self-closing tag, e.g. <img>
31 *
32 * @var string
33 */
34 protected static $frag_html_match_self_close = '/<(img) class="linguise-fragment" data-fragment-name="([^"]*)" data-fragment-param="([^"]*)" data-fragment-key="([^"]*)" data-fragment-format="((?:link|html|html-main|text|media-img|media-imgset)(?:-skip)?)" data-fragment-mode="(auto|override|skip|i18n|attribute)"(?: data-fragment-extra-id="([^"]*)")?(?: (?:href|src|srcset)="([^"]*)")?(?: linguise-ignore="1")?\s*\/?>/si';
35
36 /**
37 * Collect the fragment from the JSON data.
38 *
39 * @param string $key The key of the fragment
40 * @param mixed $value The value of the fragment
41 * @param array $collected_fragments The array of collected fragments
42 * @param string $current_key The current key of the fragment
43 * @param boolean $strict If strict mode, only "allow"-ed fragments will be collected
44 * @param integer $array_index The index of the array, if it's an array
45 *
46 * @return array - The array of collected fragments
47 */
48 private static function collectFragment($key, $value, $collected_fragments, $current_key, $strict = false, $array_index = null)
49 {
50 $use_key = self::wrapKey($key);
51 if ($array_index !== null) {
52 $use_key .= '.' . $array_index;
53 }
54
55 // Fix FragmentAttribute become invalid if value is array object
56 if ($current_key !== null && $current_key !== '') {
57 if ($key === $use_key) {
58 $use_key = '.' . $use_key;
59 }
60
61 $use_key = $current_key . $use_key;
62 }
63
64 if (Helper::isActualObject($value)) {
65 $collected_fragments = self::collectFragmentFromJson($value, $strict, $collected_fragments, $use_key);
66 } elseif (is_array($value)) {
67 for ($arr_i = 0; $arr_i < count($value); $arr_i++) { // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
68 $inner_value = $value[$arr_i];
69 $collected_fragments = self::collectFragment('', $inner_value, $collected_fragments, $use_key, $strict, $arr_i);
70 }
71 } elseif (is_string($value)) {
72 // By default, we assume "text" for now.
73 $allowed_filters = self::isKeyAllowed($key, $use_key);
74 if ($strict && ($allowed_filters !== true && !is_string($allowed_filters))) {
75 return $collected_fragments;
76 }
77 if (!is_null($allowed_filters) && $allowed_filters === false) {
78 // If it's not null and it's false, then we don't need to check further
79 return $collected_fragments;
80 }
81 $tl_string = self::isTranslatableString($value);
82 $tl_link = self::isStringLink($value);
83 $tl_dom = self::isHTMLElement($value);
84
85 $format = 'text';
86 if ($tl_link) {
87 $format = 'link';
88 if (self::isImageLink($value)) {
89 $format = 'media-img';
90 }
91 } elseif (is_string($tl_dom)) {
92 $format = $tl_dom;
93 }
94 if (is_string($allowed_filters)) {
95 $format = $allowed_filters;
96 }
97 if ($tl_string || $tl_link || is_string($tl_dom) || $allowed_filters) {
98 // Extra check
99 if (is_string($value) && !empty($value)) {
100 $collected_fragments[] = [
101 'key' => $use_key,
102 'value' => $value,
103 'format' => $format,
104 ];
105 }
106 }
107 }
108
109 return $collected_fragments;
110 }
111
112 /**
113 * A recursive function that iterates through the json data and collects the fragments
114 *
115 * @param array $json_data The JSON data to be iterated
116 * @param boolean $strict Default to false, if True this will only collect "allow"-ed fragments
117 * @param array $collected_fragments Default to empty array, when it's being called inside the function it will be appended for the parent key
118 * @param string $current_key Default to empty string, when it's being called inside the function it will be appended for the parent key
119 *
120 * @return array
121 */
122 public static function collectFragmentFromJson($json_data, $strict = false, $collected_fragments = null, $current_key = null)
123 {
124 // set default if null
125 if ($collected_fragments === null) {
126 $collected_fragments = [];
127 }
128 if ($current_key === null) {
129 $current_key = '';
130 }
131 if (empty($json_data) || !is_array($json_data)) {
132 return $collected_fragments;
133 }
134 foreach ($json_data as $key => $value) {
135 $collected_fragments = self::collectFragment($key, $value, $collected_fragments, $current_key, $strict);
136 }
137 return $collected_fragments;
138 }
139
140 /**
141 * Protect sprintf templates in the text by wrapping them in a span tag
142 *
143 * @param string $text The text to be protected
144 *
145 * @return string The protected text
146 */
147 private static function protectSprintfTemplates($text)
148 {
149 $pattern = '/(%(?:\d+\$)?[+-]?(?:\d+)?(?:\.\d+)?[bcdeEfFgGosuxX])/';
150 $replacement = '<span-ling translate="no">$1</span-ling>';
151 $result = preg_replace($pattern, $replacement, $text);
152 if (!empty($result)) {
153 return $result;
154 }
155 return $text;
156 }
157
158 /**
159 * Restore sprintf templates in the text by unwrapping them from the span tag
160 *
161 * @param string $text The text to be restored
162 *
163 * @return string The restored text
164 */
165 private static function restoreSprintfTemplates($text)
166 {
167 $pattern = '/<span-ling translate="no">(%(?:\d+\$)?[+-]?(?:\d+)?(?:\.\d+)?[bcdeEfFgGosuxX])<\/span-ling>/';
168 $result = preg_replace($pattern, '$1', $text);
169 if (!empty($result)) {
170 return $result;
171 }
172 return $text;
173 }
174
175 /**
176 * Convert the array fragments created previously into a HTML
177 * data that will be injected into the page.
178 *
179 * @param string $fragment_name The name of the fragment, e.g. 'woocommerce'
180 * @param string $fragment_param The param of the fragment, e.g. 'params' used in the actual variables
181 * @param array $json_fragments The array of fragments, generated with collectFragmentFromJson
182 *
183 * @return string - The HTML data that will be injected into the page
184 */
185 public static function intoHTMLFragments($fragment_name, $fragment_param, $json_fragments)
186 {
187 $html = '';
188 foreach ($json_fragments['fragments'] as $fragment) {
189 $tag = 'div';
190 if ($fragment['format'] === 'link') {
191 $tag = 'a';
192 }
193 $frag_value = $fragment['value'];
194 if ($fragment['format'] === 'html') {
195 // check if html has div, if yes change it to divlinguise
196 $frag_value = preg_replace('/<div(.*?)>(.*?)<\/div>$/si', '<linguise-div$1>$2</linguise-div>', $frag_value, 1);
197 }
198 if ($fragment['format'] === 'html-main') {
199 $tag = 'linguise-main';
200 }
201 if ($fragment['format'] === 'media-img' || $fragment['format'] === 'media-imgset') {
202 $tag = 'img';
203 }
204 $frag_format = $fragment['format'];
205 if (isset($fragment['skip']) && $fragment['skip']) {
206 $frag_format .= '-skip';
207 }
208
209 $html .= '<' . $tag . ' class="linguise-fragment" data-fragment-name="' . $fragment_name . '" data-fragment-param="' . $fragment_param . '" data-fragment-key="';
210 $html .= $fragment['key'] . '" data-fragment-format="' . $frag_format . '" data-fragment-mode="' . $json_fragments['mode'] . '"';
211 if ($json_fragments['mode'] === 'attribute' && isset($json_fragments['attribute'])) {
212 $html .= ' data-fragment-extra-id="' . $json_fragments['attribute'] . '"';
213 }
214 if ($json_fragments['mode'] === 'i18n' && isset($fragment['index'])) {
215 $html .= ' data-fragment-extra-id="' . $fragment['index'] . '"';
216 }
217 if ($fragment['format'] === 'link') {
218 $html .= ' href="' . $fragment['value'] . '">';
219 } elseif ($fragment['format'] === 'media-img' || $fragment['format'] === 'media-imgset') {
220 $tag_select = $fragment['format'] === 'media-imgset' ? 'srcset' : 'src';
221 $html .= ' ' . $tag_select . '="' . $fragment['value'] . '">';
222 } else {
223 $html .= '>' . self::protectSprintfTemplates($frag_value);
224 }
225 $html .= '</' . $tag . '>' . "\n";
226 }
227
228 return trim($html) . "\n";
229 }
230
231 /**
232 * Convert back the translated fragments into the original JSON data
233 *
234 * @param string $html_fragments The HTML fragments that was injected into the page
235 *
236 * @return array - The array of fragments, generated with collectFragmentFromJson
237 */
238 public static function intoJSONFragments($html_fragments)
239 {
240 $fragments = [];
241 // Let's just use regex to get anything with "linguise-fragment" class
242 preg_match_all(self::$frag_html_match, $html_fragments, $std_matches, PREG_SET_ORDER, 0);
243 preg_match_all(self::$frag_html_match_self_close, $html_fragments, $self_close_matches, PREG_SET_ORDER, 0);
244 $matches = array_merge($std_matches, $self_close_matches);
245 foreach ($matches as $match) {
246 $fragment_name = $match[2];
247 $fragment_param = $match[3];
248 $fragment_key = $match[4];
249 $fragment_format = $match[5];
250 $fragment_mode = $match[6];
251
252 $fragment_value = isset($match[9]) ? $match[9] : '';
253
254 $is_skipped = false;
255 if (substr($fragment_format, -5) === '-skip') {
256 $is_skipped = true;
257 $fragment_format = substr($fragment_format, 0, -5);
258 }
259
260 if ($fragment_format === 'link' || $fragment_format === 'media-img' || $fragment_format === 'media-imgset') {
261 $fragment_value = $match[8];
262 }
263
264 if ($fragment_format === 'html') {
265 // parse back the linguise-dev
266 $fragment_value = preg_replace('/<linguise-div(.*?)>(.*?)<\/linguise-div>$/si', '<div$1>$2</div>', $fragment_value, 1);
267 } elseif ($fragment_format === 'html-main') {
268 // parse back the linguise-main
269 $fragment_value = preg_replace('/<linguise-main>(.*?)<\/linguise-main>$/si', '$1', $fragment_value, 1);
270 }
271
272 // Strip any data-editor="linguise" wrap
273 $result_strip = preg_replace('/^\s*<span data-editor="linguise" data-linguise-hash="(?:.*?)">(.*?)<\/span>$/', '$1', $fragment_value, 1);
274 if (!empty($result_strip)) {
275 $fragment_value = $result_strip;
276 }
277
278 $fragment_value = self::restoreSprintfTemplates($fragment_value);
279
280 if (!isset($fragments[$fragment_name])) {
281 $fragments[$fragment_name] = [];
282 }
283 if (!isset($fragments[$fragment_name][$fragment_param])) {
284 $temp_fragment_sets = [
285 'mode' => $fragment_mode,
286 'fragments' => [],
287 ];
288
289 if ($fragment_mode === 'attribute' && isset($match[7]) && !empty($match[7])) {
290 $temp_fragment_sets['attribute'] = $match[7];
291 }
292
293 $fragments[$fragment_name][$fragment_param] = $temp_fragment_sets;
294 }
295
296 // The returned data is encoded HTML entities for non-ASCII characters
297 // Decode it back to UTF-8 for link and text, for HTML since it would be rendered
298 // the browser will decode it back to UTF-8 automatically
299 if ($fragment_format !== 'html' && $fragment_format !== 'html-main') {
300 $fragment_value = html_entity_decode($fragment_value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
301 }
302
303 $fragment_index = null;
304 if ($fragment_mode === 'i18n' && isset($match[7]) && is_numeric($match[7])) {
305 $fragment_index = (int)$match[7];
306 }
307
308 // make it into list for each fragment name
309 $fragments[$fragment_name][$fragment_param]['fragments'][] = [
310 'key' => $fragment_key,
311 'value' => $fragment_value,
312 'format' => $fragment_format,
313 'match' => $match[0],
314 'index' => $fragment_index,
315 'skip' => $is_skipped, // If `skip` is true, then don't replace this fragment (but remove it)
316 ];
317 }
318
319 Debug::log('FragmentHandler -> Dump fragments: ' . json_encode($fragments, JSON_PRETTY_PRINT));
320
321 return $fragments;
322 }
323
324 /**
325 * === HTML Injector Related ===
326 */
327
328 /**
329 * Try to match the script with the override list.
330 *
331 * @param \DOMNode|\DOMElement $script The script to be matched
332 * @param string $html_data The raw HTML data to be checked
333 *
334 * @return array|null
335 */
336 private static function tryMatchWithOverride($script, $html_data)
337 {
338 return FragmentOverrideHandler::tryMatchWithOverride($script, $html_data);
339 }
340
341 /**
342 * Parse the translation block from the script tag.
343 *
344 * @param \DOMNode|\DOMElement $script The script to be matched
345 *
346 * @return array|null
347 */
348 public static function tryMatchTranslationBlock($script)
349 {
350 return FragmentI18nHandler::tryMatchTranslationBlock($script);
351 }
352
353 /**
354 * Parse the HTML input or data into the fragments.
355 * XXX: Maybe normal regex and not DOMDocument if possible.
356 *
357 * @param string $html_data The HTML data to be parsed
358 *
359 * @return array
360 */
361 public static function findWPFragments(&$html_data)
362 {
363 $html_dom = HTMLHelper::loadHTML($html_data);
364 if (empty($html_dom)) {
365 return [];
366 }
367
368 $scripts = $html_dom->getElementsByTagName('script');
369
370 $all_fragments = [];
371 foreach ($scripts as $script) {
372 $attr_id = $script->getAttribute('id');
373 $match_res = preg_match('/^(.*)-js-extra$/', $attr_id, $attr_match);
374 if ($match_res === false || $match_res === 0) {
375 $overridden_temp = self::tryMatchWithOverride($script, $html_data);
376 if (is_array($overridden_temp)) {
377 foreach ($overridden_temp as $overridden_temp_item) {
378 $all_fragments[$overridden_temp_item['name']][$overridden_temp_item['name']] = [
379 'mode' => 'override',
380 'fragments' => $overridden_temp_item['fragments'],
381 ];
382 }
383
384 continue;
385 }
386
387 // Try matching -js-translations
388 $trans_match_res = preg_match('/^(.*)-js-translations$/', $attr_id, $trans_attr_match);
389 if ($trans_match_res !== false && $trans_match_res !== 0) {
390 $translation_block = self::tryMatchTranslationBlock($script);
391 $param_name = $trans_attr_match[1];
392 if (is_array($translation_block)) {
393 $all_fragments[$param_name][$translation_block['name']] = [
394 'mode' => 'i18n',
395 'fragments' => $translation_block['fragments'],
396 ];
397 }
398 }
399 continue;
400 }
401
402 $frag_id = $attr_match[1];
403
404 $script_content = HTMLHelper::unclobberCdataInternal($script->textContent);
405
406 $match_res = preg_match('/var ' . preg_quote(str_replace('-', '_', $frag_id), '/') . '_params = (.*);/', $script_content, $json_matches);
407 if ($match_res === false || $match_res === 0) {
408 $unmatched_res = preg_match_all('/var (.+)_params = (.*);/', $script_content, $json_multi_matches, PREG_SET_ORDER, 0);
409 if ($unmatched_res === false || $unmatched_res === 0) {
410 $overridden_temp = self::tryMatchWithOverride($script, $html_data);
411 if (is_array($overridden_temp)) {
412 foreach ($overridden_temp as $overridden_temp_item) {
413 $all_fragments[$frag_id][$overridden_temp_item['name']] = [
414 'mode' => 'override',
415 'fragments' => $overridden_temp_item['fragments'],
416 ];
417 }
418 }
419 continue;
420 }
421
422 // Since this is might have multiple matches, we need to check each of them
423 foreach ($json_multi_matches as $json_multi_match) {
424 $json_par_key = $json_multi_match[1];
425 $json_data = $json_multi_match[2];
426 $collected_temp = self::collectFragmentFromJson(json_decode($json_data, true));
427 if (!empty($collected_temp)) {
428 $all_fragments[$frag_id][$json_par_key] = [
429 'mode' => 'auto',
430 'fragments' => $collected_temp,
431 ];
432 }
433 }
434
435 continue;
436 }
437
438 $json_data = $json_matches[1];
439 $collected_temp = self::collectFragmentFromJson(json_decode($json_data, true));
440 if (!empty($collected_temp)) {
441 $all_fragments[$frag_id][str_replace('-', '_', $frag_id)] = [
442 'mode' => 'auto',
443 'fragments' => $collected_temp,
444 ];
445 }
446 }
447
448 // Run filters
449 $filtered_fragments = $all_fragments;
450 $filtered_fragments = apply_filters('linguise_after_fragment_collection', $filtered_fragments, $html_data, $html_dom);
451 if (is_array($filtered_fragments)) {
452 $all_fragments = $filtered_fragments;
453 }
454
455 Debug::log('FragmentHandler -> Collected: ' . json_encode($all_fragments, JSON_PRETTY_PRINT));
456
457 return $all_fragments;
458 }
459
460 /**
461 * Apply the translated fragments for the override.
462 *
463 * @param string $html_data The HTML data to be injected
464 * @param string $fragment_name The name of the fragment, e.g. 'woocommerce'
465 * @param string $fragment_param The param of the fragment, e.g. 'woocommerce$$attr-1'
466 * @param array $fragment_info The array of fragments to be injected, from intoJSONFragments
467 *
468 * @return string
469 */
470 public static function applyTranslatedFragmentsForOverride($html_data, $fragment_name, $fragment_param, $fragment_info)
471 {
472 return FragmentOverrideHandler::applyTranslatedFragmentsForOverride($html_data, $fragment_name, $fragment_param, $fragment_info);
473 }
474
475 /**
476 * Apply the translated fragments for the auto mode.
477 *
478 * @param array $json_data The JSON data to be injected
479 * @param array $fragments The array of fragments to be injected, from intoJSONFragments
480 *
481 * @return array
482 */
483 public static function applyTranslatedFragmentsForAuto($json_data, $fragments)
484 {
485 return FragmentAutoHandler::applyTranslatedFragmentsForAuto($json_data, $fragments);
486 }
487
488 /**
489 * Apply the translated fragments for the i18n mode.
490 *
491 * @param string $html_data The HTML data to be injected
492 * @param string $param_name The name of the fragment, e.g. 'woocommerce'
493 * @param string $param_key The param of the fragment, e.g. 'messages'
494 * @param array $fragments The array of fragments to be injected, from intoJSON
495 *
496 * @return string
497 */
498 public static function applyTranslatedFragmentsForI18n($html_data, $param_name, $param_key, $fragments)
499 {
500 return FragmentI18nHandler::applyTranslatedFragmentsForI18n($html_data, $param_name, $param_key, $fragments);
501 }
502
503 /**
504 * Inject the fragments into the HTML data.
505 *
506 * @param string $html_data The HTML data to be injected
507 * @param array $fragments The array of fragments to be injected, from intoJSONFragments
508 *
509 * @return string
510 */
511 public static function injectTranslatedWPFragments($html_data, $fragments)
512 {
513 Debug::log('FragmentHandler -> Injecting: ' . json_encode($fragments, JSON_PRETTY_PRINT));
514
515 // Remove the `linguise-fragment` marker elements for every group we're about to process here,
516 // directly in the DOM. This is done once, up front, so it's unaffected by the string-based
517 // substitutions below (see FragmentBase::removeFragmentMarkers() for why this can't be a
518 // literal str_replace() against the raw captured HTML).
519 $html_dom = null;
520 foreach ($fragments as $fragment_name => $fragment_jsons) {
521 foreach ($fragment_jsons as $fragment_param => $fragment_list) {
522 if (!in_array($fragment_list['mode'], ['auto', 'override', 'skip', 'i18n'])) {
523 continue;
524 }
525
526 if ($html_dom === null) {
527 $html_dom = HTMLHelper::loadHTML($html_data);
528 if (empty($html_dom)) {
529 break 2;
530 }
531 }
532
533 self::removeFragmentMarkers($html_dom, $fragment_name, $fragment_param);
534 }
535 }
536 if (!empty($html_dom)) {
537 $html_data = HTMLHelper::saveHTML($html_dom);
538 }
539
540 foreach ($fragments as $fragment_name => $fragment_jsons) {
541 foreach ($fragment_jsons as $fragment_param => $fragment_list) {
542 $mode = $fragment_list['mode'];
543
544 if (!in_array($mode, ['auto', 'override', 'skip', 'i18n'])) {
545 continue;
546 }
547
548 if ($mode === 'skip') {
549 continue;
550 }
551
552 if ($mode === 'override') {
553 $html_data = self::applyTranslatedFragmentsForOverride($html_data, $fragment_param, $fragment_name, $fragment_list);
554 continue;
555 }
556
557 if ($mode === 'i18n') {
558 // i18n mode for translation blocks
559 $html_data = self::applyTranslatedFragmentsForI18n($html_data, $fragment_name, $fragment_param, $fragment_list['fragments']);
560 continue;
561 }
562
563 $id = preg_quote($fragment_name . '-js-extra', '/');
564 $variable = preg_quote($fragment_param . '_params', '/');
565
566 $match_ptrn = '/<script (type=[\'"][\w\/]+[\'"] )?id=[\'"]' . $id . '[\'"]>(.*?)var ' . $variable . ' = \{(.*?)\};(.*?)<\/script>/s';
567
568 preg_match($match_ptrn, $html_data, $html_matches);
569 if (empty($html_matches)) {
570 continue;
571 }
572
573 $replaced_json = self::applyTranslatedFragmentsForAuto(json_decode('{' . $html_matches[3] . '}', true), $fragment_list['fragments']);
574
575 $replaced_json = apply_filters('linguise_after_apply_translated_fragments_auto', $replaced_json, $fragment_name);
576
577 if ($replaced_json === false) {
578 throw new \LogicException('FragmentHandler -> Injection -> ' . $fragment_name . '/' . $fragment_param . ' -> JSON data is empty!');
579 }
580
581 $subst_ptrn = '<script $1id="' . $fragment_name . '-js-extra">$2var ' . $fragment_param . '_params = ' . json_encode($replaced_json) . ';$4</script>';
582
583 $replacement = preg_replace($match_ptrn, $subst_ptrn, $html_data, 1, $count);
584 if ($count) {
585 $html_data = $replacement;
586 }
587 }
588 }
589
590 $mod_html_data = apply_filters('linguise_after_fragment_translation', $html_data, $fragments);
591 if (!empty($mod_html_data)) {
592 $html_data = $mod_html_data;
593 }
594 return $html_data;
595 }
596 }
597