xref: /plugin/strata/syntax/entry.php (revision acfca0a38a41ae4d1ef9a1c2b49f54eb7f4e64b9)
15153720fSfkaag71<?php
25153720fSfkaag71/**
35153720fSfkaag71 * Strata, data entry plugin
45153720fSfkaag71 *
55153720fSfkaag71 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
65153720fSfkaag71 * @author     Brend Wanders <b.wanders@utwente.nl>
75153720fSfkaag71 */
85153720fSfkaag71
95153720fSfkaag71if (!defined('DOKU_INC')) die('Meh.');
10*acfca0a3SFKaaguse dokuwiki\Utf8\Clean;
115153720fSfkaag71
125153720fSfkaag71/**
135153720fSfkaag71 * Data entry syntax for dedicated data blocks.
145153720fSfkaag71 */
155153720fSfkaag71class syntax_plugin_strata_entry extends DokuWiki_Syntax_Plugin {
165153720fSfkaag71    protected static $previewMetadata = array();
175153720fSfkaag71
185153720fSfkaag71    function __construct() {
195153720fSfkaag71        $this->syntax =& plugin_load('helper', 'strata_syntax');
205153720fSfkaag71        $this->util =& plugin_load('helper', 'strata_util');
215153720fSfkaag71        $this->triples =& plugin_load('helper', 'strata_triples');
225153720fSfkaag71    }
235153720fSfkaag71
245153720fSfkaag71    function getType() {
255153720fSfkaag71        return 'substition';
265153720fSfkaag71    }
275153720fSfkaag71
285153720fSfkaag71    function getPType() {
295153720fSfkaag71        return 'block';
305153720fSfkaag71    }
315153720fSfkaag71
325153720fSfkaag71    function getSort() {
335153720fSfkaag71        return 450;
345153720fSfkaag71    }
355153720fSfkaag71
365153720fSfkaag71    function connectTo($mode) {
375153720fSfkaag71        if($this->getConf('enable_entry')) {
385153720fSfkaag71            $this->Lexer->addSpecialPattern('<data(?: +[^#>]+?)?(?: *#[^>]*?)?>\s*?\n(?:.*?\n)*?\s*?</data>',$mode, 'plugin_strata_entry');
395153720fSfkaag71        }
405153720fSfkaag71    }
415153720fSfkaag71
425153720fSfkaag71    function handle($match, $state, $pos, Doku_Handler $handler) {
435153720fSfkaag71        $result = array(
445153720fSfkaag71            'entry'=>'',
455153720fSfkaag71            'data'=> array(
465153720fSfkaag71                $this->util->getIsaKey(false) => array(),
475153720fSfkaag71                $this->util->getTitleKey(false) => array()
485153720fSfkaag71            )
495153720fSfkaag71        );
505153720fSfkaag71
515153720fSfkaag71        // allow for preprocessing by a subclass
525153720fSfkaag71        $match = $this->preprocess($match, $state, $pos, $handler, $result);
535153720fSfkaag71
545153720fSfkaag71        $lines = explode("\n",$match);
555153720fSfkaag71        $header = trim(array_shift($lines));
565153720fSfkaag71        $footer = trim(array_pop($lines));
575153720fSfkaag71
585153720fSfkaag71
595153720fSfkaag71        // allow subclasses to mangle header
605153720fSfkaag71        $header = $this->handleHeader($header, $result);
615153720fSfkaag71
625153720fSfkaag71        // extract header, and match it to get classes and fragment
635153720fSfkaag71        preg_match('/^( +[^#>]+)?(?: *#([^>]*?))?$/', $header, $header);
645153720fSfkaag71
655153720fSfkaag71        // process the classes into triples
660847ebd2SFKaag        if (isset($header[1]))
670847ebd2SFKaag        {
685153720fSfkaag71        foreach(preg_split('/\s+/',trim($header[1])) as $class) {
695153720fSfkaag71            if($class == '') continue;
705153720fSfkaag71            $result['data'][$this->util->getIsaKey(false)][] = array('value'=>$class,'type'=>'text', 'hint'=>null);
715153720fSfkaag71        }
720847ebd2SFKaag        }
735153720fSfkaag71
745153720fSfkaag71        // process the fragment if necessary
750847ebd2SFKaag        $result['entry'] = $header[2]??null;
765153720fSfkaag71        $result['position'] = $pos;
775153720fSfkaag71        if($result['entry'] != '') {
785153720fSfkaag71            $result['title candidate'] = array('value'=>$result['entry'], 'type'=>'text', 'hint'=>null);
795153720fSfkaag71        }
805153720fSfkaag71
815153720fSfkaag71        // parse tree
825153720fSfkaag71        $tree = $this->syntax->constructTree($lines,'data entry');
835153720fSfkaag71
845153720fSfkaag71        // allow subclasses first pick in the tree
855153720fSfkaag71        $this->handleBody($tree, $result);
865153720fSfkaag71
875153720fSfkaag71        // fetch all lines
885153720fSfkaag71        $lines = $this->syntax->extractText($tree);
895153720fSfkaag71
905153720fSfkaag71        // sanity check
915153720fSfkaag71        if(count($tree['cs'])) {
925153720fSfkaag71            msg(sprintf($this->syntax->getLang('error_entry_block'), ($tree['cs'][0]['tag']?sprintf($this->syntax->getLang('named_group'),utf8_tohtml(hsc($tree['cs'][0]['tag']))):$this->syntax->getLang('unnamed_group')), utf8_tohtml(hsc($result['entry']))),-1);
935153720fSfkaag71            return array();
945153720fSfkaag71        }
955153720fSfkaag71
965153720fSfkaag71        $p = $this->syntax->getPatterns();
975153720fSfkaag71
985153720fSfkaag71        // now handle all lines
995153720fSfkaag71        foreach($lines as $line) {
1005153720fSfkaag71            $line = $line['text'];
1015153720fSfkaag71            // match a "property_type(hint)*: value" pattern
1025153720fSfkaag71            // (the * is only used to indicate that the value is actually a comma-seperated list)
1035153720fSfkaag71            // [grammar] ENTRY := PREDICATE TYPE? '*'? ':' ANY
1045153720fSfkaag71            if(preg_match("/^({$p->predicate})\s*({$p->type})?\s*(\*)?\s*:\s*({$p->any}?)$/",$line,$parts)) {
1055153720fSfkaag71                // assign useful names
1065153720fSfkaag71                list(, $property, $ptype, $multi, $values) = $parts;
1075153720fSfkaag71                list($type,$hint) = $p->type($ptype);
1085153720fSfkaag71
1095153720fSfkaag71                // trim property so we don't get accidental 'name   ' keys
1105153720fSfkaag71                $property = utf8_trim($property);
1115153720fSfkaag71
1125153720fSfkaag71                // lazy create key bucket
1135153720fSfkaag71                if(!isset($result['data'][$property])) {
1145153720fSfkaag71                    $result['data'][$property] = array();
1155153720fSfkaag71                }
1165153720fSfkaag71
1175153720fSfkaag71                // determine values, splitting on commas if necessary
1185153720fSfkaag71                $values = ($multi == '*') ? explode(',',$values) : array($values);
1195153720fSfkaag71
1205153720fSfkaag71                // generate triples from the values
1215153720fSfkaag71                foreach($values as $v) {
1225153720fSfkaag71                    $v = utf8_trim($v);
1235153720fSfkaag71                    if($v == '') continue;
1245153720fSfkaag71                    // replace the [[]] quasi-magic token with the empty string
1255153720fSfkaag71                    if($v == '[[]]') $v = '';
1265153720fSfkaag71                    if(!isset($type) || $type == '') {
1275153720fSfkaag71                        list($type, $hint) = $this->util->getDefaultType();
1285153720fSfkaag71                    }
1295153720fSfkaag71                    $result['data'][$property][] = array('value'=>$v,'type'=>$type,'hint'=>($hint?:null));
1305153720fSfkaag71                }
1315153720fSfkaag71            } else {
1325153720fSfkaag71                msg(sprintf($this->syntax->getLang('error_entry_line'), utf8_tohtml(hsc($line))),-1);
1335153720fSfkaag71            }
1345153720fSfkaag71        }
1355153720fSfkaag71
1365153720fSfkaag71        // normalize data:
1375153720fSfkaag71        // - Normalize all values
1385153720fSfkaag71        $buckets = $result['data'];
1395153720fSfkaag71        $result['data'] = array();
1405153720fSfkaag71
1415153720fSfkaag71        foreach($buckets as $property=>&$bucket) {
1425153720fSfkaag71            // normalize the predicate
1435153720fSfkaag71            $property = $this->util->normalizePredicate($property);
1445153720fSfkaag71
1455153720fSfkaag71            // process all triples
1465153720fSfkaag71            foreach($bucket as &$triple) {
1475153720fSfkaag71                // normalize the value
1485153720fSfkaag71                $type = $this->util->loadType($triple['type']);
1495153720fSfkaag71                $triple['value'] = $type->normalize($triple['value'], $triple['hint']);
1505153720fSfkaag71
1515153720fSfkaag71                // lazy create property bucket
1525153720fSfkaag71                if(!isset($result['data'][$property])) {
1535153720fSfkaag71                    $result['data'][$property] = array();
1545153720fSfkaag71                }
1555153720fSfkaag71
1565153720fSfkaag71                $result['data'][$property][] = $triple;
1575153720fSfkaag71            }
1585153720fSfkaag71        }
1595153720fSfkaag71
1605153720fSfkaag71
1615153720fSfkaag71        // normalize title candidate
1625153720fSfkaag71        if(!empty($result['title candidate'])) {
1635153720fSfkaag71            $type = $this->util->loadType($result['title candidate']['type']);
1645153720fSfkaag71            $result['title candidate']['value'] = $type->normalize($result['title candidate']['value'], $result['title candidate']['hint']);
1655153720fSfkaag71        }
1665153720fSfkaag71
1675153720fSfkaag71        $footer = $this->handleFooter($footer, $result);
1685153720fSfkaag71
1695153720fSfkaag71        return $result;
1705153720fSfkaag71    }
1715153720fSfkaag71
1725153720fSfkaag71    /**
1735153720fSfkaag71     * Handles the whole match. This method is called before any processing
1745153720fSfkaag71     * is done by the actual class.
1755153720fSfkaag71     *
1765153720fSfkaag71     * @param match string the complete match
1775153720fSfkaag71     * @param state the parser state
1785153720fSfkaag71     * @param pos the position in the source
1795153720fSfkaag71     * @param the handler object
1805153720fSfkaag71     * @param result array the result array passed to the render method
1815153720fSfkaag71     * @return a preprocessed string
1825153720fSfkaag71     */
1835153720fSfkaag71    function preprocess($match, $state, $pos, &$handler, &$result) {
1845153720fSfkaag71        return $match;
1855153720fSfkaag71    }
1865153720fSfkaag71
1875153720fSfkaag71    /**
1885153720fSfkaag71     * Handles the header of the syntax. This method is called before
1895153720fSfkaag71     * the header is handled.
1905153720fSfkaag71     *
1915153720fSfkaag71     * @param header string the complete header
1925153720fSfkaag71     * @param result array the result array passed to the render method
1935153720fSfkaag71     * @return a string containing the unhandled parts of the header
1945153720fSfkaag71     */
1955153720fSfkaag71    function handleHeader($header, &$result) {
1965153720fSfkaag71        // remove prefix and suffix
1975153720fSfkaag71        return preg_replace('/(^<data)|( *>$)/','',$header);
1985153720fSfkaag71    }
1995153720fSfkaag71
2005153720fSfkaag71    /**
2015153720fSfkaag71     * Handles the body of the syntax. This method is called before any
2025153720fSfkaag71     * of the body is handled.
2035153720fSfkaag71     *
2045153720fSfkaag71     * @param tree array the parsed tree
2055153720fSfkaag71     * @param result array the result array passed to the render method
2065153720fSfkaag71     */
2075153720fSfkaag71    function handleBody(&$tree, &$result) {
2085153720fSfkaag71    }
2095153720fSfkaag71
2105153720fSfkaag71    /**
2115153720fSfkaag71     * Handles the footer of the syntax. This method is called after the
2125153720fSfkaag71     * data has been parsed and normalized.
2135153720fSfkaag71     *
2145153720fSfkaag71     * @param footer string the footer string
2155153720fSfkaag71     * @param result array the result array passed to the render method
2165153720fSfkaag71     * @return a string containing the unhandled parts of the footer
2175153720fSfkaag71     */
2185153720fSfkaag71    function handleFooter($footer, &$result) {
2195153720fSfkaag71        return '';
2205153720fSfkaag71    }
2215153720fSfkaag71
2225153720fSfkaag71
2235153720fSfkaag71    protected function getPositions($data) {
2245153720fSfkaag71        global $ID;
2255153720fSfkaag71
2265153720fSfkaag71        // determine positions of other data entries
2275153720fSfkaag71        // (self::$previewMetadata is only filled if a preview_metadata was run)
2285153720fSfkaag71        if(isset(self::$previewMetadata[$ID])) {
2295153720fSfkaag71            $positions = self::$previewMetadata[$ID]['strata']['positions'];
2305153720fSfkaag71        } else {
2315153720fSfkaag71            $positions = p_get_metadata($ID, 'strata positions');
2325153720fSfkaag71        }
2335153720fSfkaag71
2345153720fSfkaag71        // only read positions if we have them
2355153720fSfkaag71        if(is_array($positions) && isset($positions[$data['entry']])) {
2365153720fSfkaag71            $positions = $positions[$data['entry']];
2375153720fSfkaag71            $currentPosition = array_search($data['position'],$positions);
2385153720fSfkaag71            $previousPosition = isset($positions[$currentPosition-1])?'data_fragment_'.$positions[$currentPosition-1]:null;
2395153720fSfkaag71            $nextPosition = isset($positions[$currentPosition+1])?'data_fragment_'.$positions[$currentPosition+1]:null;
2405153720fSfkaag71            $currentPosition = 'data_fragment_'.$positions[$currentPosition];
2415153720fSfkaag71        }
2425153720fSfkaag71
2435153720fSfkaag71        return array($currentPosition, $previousPosition, $nextPosition);
2445153720fSfkaag71    }
2455153720fSfkaag71
2465153720fSfkaag71    function render($mode, Doku_Renderer $R, $data) {
2475153720fSfkaag71        global $ID;
2485153720fSfkaag71
2495153720fSfkaag71        if($data == array()) {
2505153720fSfkaag71            return false;
2515153720fSfkaag71        }
2525153720fSfkaag71
2535153720fSfkaag71        if($mode == 'xhtml' || $mode == 'odt') {
2545153720fSfkaag71
2555153720fSfkaag71            // determine actual header text
2565153720fSfkaag71            $heading = '';
2575153720fSfkaag71            if(isset($data['data'][$this->util->getTitleKey()])) {
2585153720fSfkaag71                // use title triple if possible
2595153720fSfkaag71                $heading = $data['data'][$this->util->getTitleKey()][0]['value'];
2605153720fSfkaag71            } elseif (!empty($data['title candidate'])) {
2615153720fSfkaag71                // use title candidate if possible
2625153720fSfkaag71                $heading = $data['title candidate']['value'];
2635153720fSfkaag71            } else {
2645153720fSfkaag71                if(useHeading('content')) {
2655153720fSfkaag71                    // fall back to page title, depending on wiki configuration
2665153720fSfkaag71                    $heading = p_get_first_heading($ID);
2675153720fSfkaag71                }
2685153720fSfkaag71
2695153720fSfkaag71                if(!$heading) {
2705153720fSfkaag71                    // use page id if all else fails
2715153720fSfkaag71                    $heading = noNS($ID);
2725153720fSfkaag71                }
2735153720fSfkaag71            }
274*acfca0a3SFKaag
275*acfca0a3SFKaag            list($currentPosition, $previousPosition, $nextPosition) = $this->getPositions($data);
276*acfca0a3SFKaag            // render table header
277*acfca0a3SFKaag            if($mode == 'xhtml') { $R->doc .= '<div class="strata-entry" id="'.Clean::deaccent(strtolower($heading)).'">'; }
278*acfca0a3SFKaag            if($mode == 'odt' && isset($currentPosition) && method_exists ($R, 'insertBookmark')) {
279*acfca0a3SFKaag                $R->insertBookmark($currentPosition, false);
280*acfca0a3SFKaag            }
281*acfca0a3SFKaag            $R->table_open();
282*acfca0a3SFKaag            $R->tablerow_open();
283*acfca0a3SFKaag            $R->tableheader_open(2);
284*acfca0a3SFKaag
2855153720fSfkaag71            $R->cdata($heading);
2865153720fSfkaag71
2875153720fSfkaag71            // display a comma-separated list of classes if the entry has classes
2885153720fSfkaag71            if(isset($data['data'][$this->util->getIsaKey()])) {
2895153720fSfkaag71                $R->emphasis_open();
2905153720fSfkaag71                $R->cdata(' (');
2915153720fSfkaag71                $values = $data['data'][$this->util->getIsaKey()];
2925153720fSfkaag71                $this->util->openField($mode, $R, $this->util->getIsaKey());
2935153720fSfkaag71                for($i=0;$i<count($values);$i++) {
2945153720fSfkaag71                    $triple =& $values[$i];
2955153720fSfkaag71                    if($i!=0) $R->cdata(', ');
2965153720fSfkaag71                    $type = $this->util->loadType($triple['type']);
2975153720fSfkaag71                    $this->util->renderValue($mode, $R, $this->triples, $triple['value'], $triple['type'], $type, $triple['hint']);
2985153720fSfkaag71                }
2995153720fSfkaag71                $this->util->closeField($mode, $R);
3005153720fSfkaag71                $R->cdata(')');
3015153720fSfkaag71                $R->emphasis_close();
3025153720fSfkaag71            }
3035153720fSfkaag71            $R->tableheader_close();
3045153720fSfkaag71            $R->tablerow_close();
3055153720fSfkaag71
3065153720fSfkaag71            // render a row for each key, displaying the values as comma-separated list
3075153720fSfkaag71            foreach($data['data'] as $key=>$values) {
3085153720fSfkaag71                // skip isa and title keys
3095153720fSfkaag71                if($key == $this->util->getTitleKey() || $key == $this->util->getIsaKey()) continue;
3105153720fSfkaag71
3115153720fSfkaag71                // render row header
3125153720fSfkaag71                $R->tablerow_open();
3135153720fSfkaag71                $R->tableheader_open();
3145153720fSfkaag71                $this->util->renderPredicate($mode, $R, $this->triples, $key);
3155153720fSfkaag71                $R->tableheader_close();
3165153720fSfkaag71
3175153720fSfkaag71                // render row content
3185153720fSfkaag71                $R->tablecell_open();
3195153720fSfkaag71                $this->util->openField($mode, $R, $key);
3205153720fSfkaag71                for($i=0;$i<count($values);$i++) {
3215153720fSfkaag71                    $triple =& $values[$i];
3225153720fSfkaag71                    if($i!=0) $R->cdata(', ');
3235153720fSfkaag71                    $this->util->renderValue($mode, $R, $this->triples, $triple['value'], $triple['type'], $triple['hint']);
3245153720fSfkaag71                }
3255153720fSfkaag71                $this->util->closeField($mode, $R);
3265153720fSfkaag71                $R->tablecell_close();
3275153720fSfkaag71                $R->tablerow_close();
3285153720fSfkaag71            }
3295153720fSfkaag71
3305153720fSfkaag71            if($previousPosition || $nextPosition) {
3315153720fSfkaag71                $R->tablerow_open();
3325153720fSfkaag71                $R->tableheader_open(2);
3335153720fSfkaag71                if($previousPosition) {
3345153720fSfkaag71                    if($mode == 'xhtml') { $R->doc .= '<span class="strata-data-fragment-link-previous">'; }
3355153720fSfkaag71                    $R->locallink($previousPosition, $this->util->getLang('data_entry_previous'));
3365153720fSfkaag71                    if($mode == 'xhtml') { $R->doc .= '</span>'; }
3375153720fSfkaag71                }
3385153720fSfkaag71                $R->cdata(' ');
3395153720fSfkaag71                if($nextPosition) {
3405153720fSfkaag71                    if($mode == 'xhtml') { $R->doc .= '<span class="strata-data-fragment-link-next">'; }
3415153720fSfkaag71                    $R->locallink($nextPosition, $this->util->getLang('data_entry_next'));
3425153720fSfkaag71                    if($mode == 'xhtml') { $R->doc .= '</span>'; }
3435153720fSfkaag71                }
3445153720fSfkaag71                $R->tableheader_close();
3455153720fSfkaag71                $R->tablerow_close();
3465153720fSfkaag71            }
3475153720fSfkaag71
3485153720fSfkaag71            $R->table_close();
3495153720fSfkaag71            if($mode == 'xhtml') { $R->doc .= '</div>'; }
3505153720fSfkaag71
3515153720fSfkaag71            return true;
3525153720fSfkaag71
3535153720fSfkaag71        } elseif($mode == 'metadata' || $mode == 'preview_metadata') {
3545153720fSfkaag71            $triples = array();
3555153720fSfkaag71            $subject = $ID.'#'.$data['entry'];
3565153720fSfkaag71
3575153720fSfkaag71            // resolve the subject to normalize everything
3585153720fSfkaag71            resolve_pageid(getNS($ID),$subject,$exists);
3595153720fSfkaag71
3605153720fSfkaag71            $titleKey = $this->util->getTitleKey();
3615153720fSfkaag71
3625153720fSfkaag71            $fixTitle = false;
3635153720fSfkaag71
3645153720fSfkaag71            // we only use the title determination if no explicit title was given
3655153720fSfkaag71            if(empty($data['data'][$titleKey])) {
3665153720fSfkaag71                if(!empty($data['title candidate'])) {
3675153720fSfkaag71                    // we have a candidate from somewhere
3685153720fSfkaag71                    $data['data'][$titleKey][] = $data['title candidate'];
3695153720fSfkaag71                } else {
3705153720fSfkaag71                    if(!empty($R->meta['title'])) {
3715153720fSfkaag71                        // we do not have a candidate, so we use the page title
3725153720fSfkaag71                        // (this is possible because fragments set the candidate)
3735153720fSfkaag71                        $data['data'][$titleKey][] = array(
3745153720fSfkaag71                            'value'=>$R->meta['title'],
3755153720fSfkaag71                            'type'=>'text',
3765153720fSfkaag71                            'hint'=>null
3775153720fSfkaag71                        );
3785153720fSfkaag71                    } else {
3795153720fSfkaag71                        // we were added before the page title is known
3805153720fSfkaag71                        // however, we do require a page title (iff we actually store data)
3815153720fSfkaag71                        $fixTitle = true;
3825153720fSfkaag71                    }
3835153720fSfkaag71                }
3845153720fSfkaag71            }
3855153720fSfkaag71
3865153720fSfkaag71            // store positions information
3875153720fSfkaag71            if($mode == 'preview_metadata') {
3885153720fSfkaag71                self::$previewMetadata[$ID]['strata']['positions'][$data['entry']][] = $data['position'];
3895153720fSfkaag71            } else {
3905153720fSfkaag71                $R->meta['strata']['positions'][$data['entry']][] = $data['position'];
3915153720fSfkaag71            }
3925153720fSfkaag71
3935153720fSfkaag71            // process triples
3945153720fSfkaag71            foreach($data['data'] as $property=>$bucket) {
3955153720fSfkaag71                $this->util->renderPredicate($mode, $R, $this->triples, $property);
3965153720fSfkaag71
3975153720fSfkaag71                foreach($bucket as $triple) {
3985153720fSfkaag71                    // render values for things like backlinks
3995153720fSfkaag71                    $type = $this->util->loadType($triple['type']);
4005153720fSfkaag71                    $type->render($mode, $R, $this->triples, $triple['value'], $triple['hint']);
4015153720fSfkaag71
4025153720fSfkaag71                    // prepare triples for storage
4035153720fSfkaag71                    $triples[] = array('subject'=>$subject, 'predicate'=>$property, 'object'=>$triple['value']);
4045153720fSfkaag71                }
4055153720fSfkaag71            }
4065153720fSfkaag71
4075153720fSfkaag71            // we're done if nodata is flagged.
4085153720fSfkaag71            if(!isset($R->info['data']) || $R->info['data']==true) {
4095153720fSfkaag71                // batch-store triples if we're allowed to store
4105153720fSfkaag71                $this->triples->addTriples($triples, $ID);
4115153720fSfkaag71
4125153720fSfkaag71                // set flag for title addendum
4135153720fSfkaag71                if($fixTitle) {
4145153720fSfkaag71                    $R->meta['strata']['fixTitle'] = true;
4155153720fSfkaag71                }
4165153720fSfkaag71            }
4175153720fSfkaag71
4185153720fSfkaag71            return true;
4195153720fSfkaag71        }
4205153720fSfkaag71
4215153720fSfkaag71        return false;
4225153720fSfkaag71    }
4235153720fSfkaag71}
424