예제 #1
1
 /**
  * 行を解析し、text、answer、descriptionフィールドに対応する文字列を取り出します。
  * @param Dictionary $dictionary
  * @param string $line
  */
 protected function parseLine(Dictionary $dictionary, string $line)
 {
     // コメントの分離
     $textAndDescription = preg_split('#(/+|;+|\\[)#u', $line, 2, PREG_SPLIT_DELIM_CAPTURE);
     if ($textAndDescription[0] !== '' && str_replace(' ', '', $textAndDescription[0]) === '') {
         // 空行でなく、半角スペースのみで構成されている行なら
         throw new SyntaxException(_('半角スペースとコメントのみの行は作ることができません。'));
     }
     $answers = array_filter(array_map(function ($answer) {
         // 正規表現文字列扱いを抑止
         return (new \esperecyan\dictionary_php\validator\AnswerValidator())->isRegExp($answer) ? trim($answer, '/') : $answer;
     }, explode(',', rtrim($textAndDescription[0], ' '))));
     if ($answers) {
         if (count($answers) === 1) {
             $fieldsAsMultiDimensionalArray['text'] = $answers;
         } else {
             $fieldsAsMultiDimensionalArray = ['text' => [$answers[0]], 'answer' => $answers];
         }
         if (isset($textAndDescription[1])) {
             $comment = trim($textAndDescription[1] === '[' ? rtrim($textAndDescription[2], ']') : $textAndDescription[2]);
             if ($comment !== '') {
                 $fieldsAsMultiDimensionalArray['description'][] = $comment;
             }
         }
         try {
             $dictionary->addWord($fieldsAsMultiDimensionalArray);
         } catch (SyntaxException $e) {
             $this->logInconvertibleError($line, $e);
         }
     }
 }
 /**
  * @param string[][][] $fieldsAsMultiDimensionalArrays
  * @param string[] $metadata
  * @param string[] $files
  * @return Dictionary
  */
 protected function generateDictionary(array $fieldsAsMultiDimensionalArrays, array $metadata, array $files) : Dictionary
 {
     if ($files) {
         $tempDirectory = (new \esperecyan\dictionary_php\parser\GenericDictionaryParser())->generateTempDirectory();
         foreach ($files as $filename => $file) {
             file_put_contents("{$tempDirectory}/{$filename}", $file);
         }
     }
     $dictionary = new Dictionary(isset($tempDirectory) ? new \FilesystemIterator($tempDirectory) : null);
     foreach ($fieldsAsMultiDimensionalArrays as $fieldsAsMultiDimensionalArray) {
         $dictionary->addWord($fieldsAsMultiDimensionalArray);
     }
     $dictionary->setMetadata($metadata);
     return $dictionary;
 }
예제 #3
0
 /**
  * @param \SplFileInfo $file
  * @param string|null $filename
  * @param string|null $title
  * @throws SyntaxException お題の数、辞書全体の文字数が制限範囲外であるとき。
  * @return Dictionary
  */
 public function parse(\SplFileInfo $file, string $filename = null, string $title = null) : Dictionary
 {
     $dictionary = new Dictionary();
     $words = [];
     if (!$file instanceof \SplFileObject) {
         $file = $file->openFile();
     } else {
         $file->rewind();
     }
     $file->setFlags(\SplFileObject::DROP_NEW_LINE | \SplFileObject::READ_AHEAD | \SplFileObject::SKIP_EMPTY);
     foreach ($file as $line) {
         $word = $this->parseLine($line);
         if (!in_array($word, $words)) {
             $dictionary->addWord(['text' => [str_replace('ヴ', 'ゔ', $line)]]);
             $words[] = $word;
         }
     }
     $wordsLength = count($words);
     if ($wordsLength < self::WORDS_MIN) {
         throw new SyntaxException(sprintf(_('お題が%1$d個しかありません。%2$d個以上必要です。'), $wordsLength, self::WORDS_MIN));
     }
     if ($wordsLength > self::WORDS_MAX) {
         throw new SyntaxException(sprintf(_('お題が%1$d個あります。%2$d個以内にする必要があります。'), $wordsLength, self::WORDS_MAX));
     }
     $dictionaryCodePoints = mb_strlen(implode('', $words), 'UTF-8');
     if ($dictionaryCodePoints > self::DICTIONARY_CODE_POINTS_MAX) {
         throw new SyntaxException(sprintf(_('辞書全体で%1$d文字あります。%2$d文字以内にする必要があります。'), $dictionaryCodePoints, self::DICTIONARY_CODE_POINTS_MAX));
     }
     if (!is_null($title) || !is_null($filename)) {
         if (!is_null($title) && $title !== '') {
             $trimedTitle = preg_replace('/^[  \\t]+|[  \\t]+$/u', '', $title);
         }
         if (!is_null($filename) && (!isset($trimedTitle) || $trimedTitle === '')) {
             $trimedTitle = preg_replace('/^[  \\t]+|[  \\t]+$/u', '', (new GenericDictionaryParser())->getTitleFromFilename($filename));
         }
         if (isset($trimedTitle) && $trimedTitle !== '') {
             $dictionary->setMetadata(['@title' => $trimedTitle]);
             $titleLength = $this->getLengthAs16BitCodeUnits($trimedTitle);
             if ($titleLength > self::TITLE_MAX) {
                 $this->logger->error(sprintf(_('辞書名が%1$d文字 (補助文字は2文字扱い) あります。ピクトセンスにおける辞書名の最大文字数は%2$d文字です。'), $titleLength, self::TITLE_MAX));
             }
         } else {
             $this->logger->error(_('辞書名が空です。先頭末尾の空白は取り除かれます。'));
         }
     }
     return $dictionary;
 }
 /**
  * 行を解析し、textフィールドとdescriptionフィールドに対応する文字列を取り出します。
  * @param Dictionary $dictionary
  * @param string $line
  */
 protected function parseLine(Dictionary $dictionary, string $line)
 {
     // コメントの分離
     $textAndDescription = preg_split('#[\\t  ]*//#u', $line, 2);
     if (str_replace(["\t", ' ', ' '], '', $textAndDescription[0]) === '') {
         throw new SyntaxException(_('空行 (スペース、コメントのみの行) があります。'));
     } else {
         $fieldsAsMultiDimensionalArray['text'] = [(new AnswerValidator())->isRegExp($textAndDescription[0]) ? trim($textAndDescription[0], '/') : $textAndDescription[0]];
         if (isset($textAndDescription[1])) {
             // コメントが存在すれば
             $description = trim($textAndDescription[1], "  \t");
             if ($description !== '') {
                 $fieldsAsMultiDimensionalArray['description'] = [$description];
             }
         }
         try {
             $dictionary->addWord($fieldsAsMultiDimensionalArray);
         } catch (SyntaxException $e) {
             $this->logInconvertibleError($line, $e);
         }
     }
 }
 /**
  * Q&Aを解析します。
  * @param Dictionary $dictioanry
  * @param string $question
  * @param string $answer
  */
 protected function parseQuizLines(Dictionary $dictioanry, string $question, string $answer)
 {
     $line = "{$question}\n{$answer}";
     $specifics = new URLSearchParams();
     $questionFields = explode(',', $question, 5);
     $answerFields = explode(',', $answer);
     if (!$this->isNumeric($questionFields[1])) {
         throw new SyntaxException(sprintf(_('出題の種類「%s」は数値として認識できません。'), $questionFields[1]));
     } elseif (!$this->isNumeric($answerFields[1])) {
         throw new SyntaxException(sprintf(_('解答の種類「%s」は数値として認識できません。'), $answerFields[1]));
     }
     /** @var int 出題の種類。 */
     $questionType = $this->convertToInt($questionFields[1]);
     switch ($questionType) {
         case 1:
             // 音声ファイルを再生
         // 音声ファイルを再生
         case 2:
             // 画像ファイルを表示
             if (empty($questionFields[3])) {
                 throw new SyntaxException(_('ファイルが指定されていません。'));
             }
             // ファイル名
             $fieldsAsMultiDimensionalArray[$questionType === 1 ? 'audio' : 'image'][] = $questionFields[3];
             // 問題オプション
             if (isset($questionFields[4])) {
                 foreach (explode(',', $questionFields[4]) as $option) {
                     $option = explode('=', $option, 2);
                     $name = $option[0];
                     $value = $option[1] ?? '';
                     if (!$this->isNumeric($value)) {
                         throw new SyntaxException(sprintf(_('問題オプション %1$s の値「%2$s」は数値として認識できません。'), $name, $value));
                     }
                     $int = $this->convertToInt($value);
                     switch ($name) {
                         case 'start':
                         case 'media_start':
                             $specifics->set('start', $int / self::SECONDS_TO_MILISECONDS);
                             break;
                         case 'repeat':
                             $specifics->set('repeat', $int);
                             break;
                         case 'length':
                             $specifics->set('length', $int / self::SECONDS_TO_MILISECONDS);
                             break;
                         case 'speed':
                             $specifics->set('speed', $int / self::DECIMAL_TO_PERCENT);
                             break;
                         case 'zoom_start':
                             $specifics->set('magnification', $int);
                             break;
                         case 'zoom_end':
                             $specifics->set('last-magnification', $int);
                             break;
                         case 'mozaic':
                             if ($int === 1) {
                                 $specifics->set('pixelization', '');
                             } else {
                                 $specifics->delete('pixelization');
                             }
                             break;
                         case 'score':
                             $specifics->set('score', $int);
                             break;
                         case 'finalscore':
                             $specifics->set('last-score', $int);
                             break;
                     }
                 }
             }
             break;
         case 3:
             // Wikipediaクイズ
         // Wikipediaクイズ
         case 4:
             // アンサイクロペディアクイズ
             $this->logInconvertibleError("{$question}\n{$answer}");
             return;
     }
     // 問題文
     if (isset($questionFields[2])) {
         $fieldsAsMultiDimensionalArray['question'][] = $this->parseQuestionSentence($questionFields[2]);
     }
     /** @var int 解答の種類。 */
     $answerType = $this->convertToInt($answerFields[1]);
     if (in_array($answerType, [1, 2, 3])) {
         $fieldsAsMultiDimensionalArray['type'][] = 'selection';
         if ($answerType === 3) {
             $specifics->set('require-all-right', '');
         }
     } else {
         $answerType = 0;
     }
     $mode = '|';
     $answerValidator = new \esperecyan\dictionary_php\validator\AnswerValidator();
     foreach (array_slice($answerFields, 2) as $i => $field) {
         if (isset($field[0]) && $field[0] === '\\' && isset($field[1]) && $field[1] !== '\\') {
             // 解答オプション
             if ($field === '\\norandom') {
                 // 選択肢をシャッフルしない
                 if (in_array($answerType, [1, 3])) {
                     $specifics->set('no-random', '');
                 }
             } elseif ($field === '\\seikai') {
                 // 直前の選択肢を正解扱いに
                 if (in_array($answerType, [1, 3])) {
                     if (empty($fieldsAsMultiDimensionalArray['option'])) {
                         throw new SyntaxException(_('\\seikai の前には選択肢が必要です。'));
                     }
                     $fieldsAsMultiDimensionalArray['answer'][] = end($fieldsAsMultiDimensionalArray['option']);
                 }
             } elseif (preg_match('/^\\\\explain=(.+)$/u', $field, $matches) === 1) {
                 // 解説
                 $fieldsAsMultiDimensionalArray['description'][] = $this->parseQuestionSentence($matches[1]);
             } elseif ($answerType === 0 && preg_match('/^\\\\bonus=(.*)$/u', $field, $matches) === 1) {
                 // ボーナスポイント
                 if (!$this->isNumeric($matches[1])) {
                     throw new SyntaxException(sprintf(_('解答オプション \\bonus の値「%s」は数値として認識できません。'), $matches[1]));
                 }
                 if (empty($answerPattenAndBonuses['|'])) {
                     throw new SyntaxException(sprintf(_('解答オプション「%s」の前には解答本体が必要です。'), $field));
                 }
                 $bonus = $this->convertToInt($matches[1]);
                 if ($bonus !== 0) {
                     $answerPattenAndBonuses['|'][count($answerPattenAndBonuses['|']) - 1]['bonus'] = $bonus;
                 }
             }
             continue;
         }
         switch ($answerType) {
             case 0:
                 // 記述形式
                 if ($field === '[[' || $field === '||') {
                     // 正規表現
                     if (empty($answerPattenAndBonuses['|'])) {
                         throw new SyntaxException(sprintf(_('「%s」の前には解答本体が必要です。'), $field));
                     }
                     $end = count($answerPattenAndBonuses['|']) - 1;
                     $answerPattenAndBonuses['|'][$end]['regexp'] = ($field === '||' ? '.*' : '') . preg_quote($answerPattenAndBonuses['|'][$end]['body'], '/') . '.*';
                 } elseif (in_array($field, ['[', '|', ']'])) {
                     // モード変更
                     $mode = $field;
                 } elseif ($mode === '|') {
                     $answerPattenAndBonuses['|'][]['body'] = $field;
                 } else {
                     $answerPattenAndBonuses[$mode][] = $field;
                 }
                 break;
             case 1:
                 // 選択形式
             // 選択形式
             case 3:
                 // 全選択形式
                 $fieldsAsMultiDimensionalArray['option'][] = $answerValidator->isRegExp($field) ? trim($field, '/') : $field;
                 break;
             case 2:
                 // 並べ替え形式
                 if ($i % 2 === 0) {
                     $numbersAndOptions[] = [$answerValidator->isRegExp($field) ? trim($field, '/') : $field];
                 } else {
                     $number = $this->isNumeric($field) ? $this->convertToInt($field) : 0;
                     if ($number > 0) {
                         array_unshift($numbersAndOptions[count($numbersAndOptions) - 1], $number);
                     } else {
                         // 順番が0以下であれば選択肢自体を削除
                         array_pop($numbersAndOptions);
                     }
                 }
                 break;
         }
     }
     switch ($answerType) {
         case 0:
             // 記述形式
             $noRegExpAnswerExisted = false;
             foreach ($answerPattenAndBonuses['|'] as $infix) {
                 // 接頭辞の付加
                 if (isset($answerPattenAndBonuses['['])) {
                     foreach ($answerPattenAndBonuses['['] as $prefix) {
                         $tmpInfix = $infix;
                         $tmpInfix['body'] = $prefix . $tmpInfix['body'];
                         if (isset($tmpInfix['regexp'])) {
                             $tmpInfix['regexp'] = preg_quote($prefix, '/') . $tmpInfix['regexp'];
                         }
                         $answersAndBonusesWithPrefix[] = $tmpInfix;
                     }
                 } else {
                     $answersAndBonusesWithPrefix[] = $infix;
                 }
                 // 接尾辞の付加
                 if (isset($answerPattenAndBonuses[']'])) {
                     foreach ($answerPattenAndBonuses[']'] as $suffix) {
                         foreach ($answersAndBonusesWithPrefix as $answer) {
                             $answer['body'] .= $suffix;
                             if (isset($answer['regexp'])) {
                                 $answer['regexp'] .= preg_quote($prefix, '/');
                             }
                             $answersAndBonusesWithPrefixAndSuffix[] = $answer;
                         }
                     }
                 } else {
                     $answersAndBonusesWithPrefixAndSuffix = $answersAndBonusesWithPrefix;
                 }
                 unset($answersAndBonusesWithPrefix);
                 foreach ($answersAndBonusesWithPrefixAndSuffix as &$answer) {
                     if ($answerValidator->isRegExp($answer['body'])) {
                         $answer['body'] = trim($answer['body'], '/');
                         if ($answer['body'] === '') {
                             unset($answer['body']);
                         }
                     }
                     if (isset($answer['regexp']) || isset($answer['body'])) {
                         $fieldsAsMultiDimensionalArray['answer'][] = isset($answer['regexp']) ? "/{$answer['regexp']}/" : $answer['body'];
                         if (isset($answer['regexp'])) {
                             if (!isset($noRegExpAnswerAndBonus)) {
                                 $noRegExpAnswerAndBonus = ['answer' => $answer['body'], 'bonus' => isset($answer['bonus']) ? (string) $answer['bonus'] : ''];
                             }
                         } else {
                             $noRegExpAnswerExisted = true;
                         }
                         $bonuses[] = isset($answer['bonus']) ? (string) $answer['bonus'] : '';
                     }
                 }
                 unset($answersAndBonusesWithPrefixAndSuffix);
             }
             if (!$noRegExpAnswerExisted) {
                 // answerフィールドがすべて正規表現なら
                 if (isset($noRegExpAnswerAndBonus)) {
                     array_unshift($fieldsAsMultiDimensionalArray['answer'], $noRegExpAnswerAndBonus['answer']);
                     array_unshift($bonuses, $noRegExpAnswerAndBonus['bonus']);
                 } else {
                     $this->logInconvertibleError($line);
                     return;
                 }
             }
             // specificsフィールドのbonusの設定
             if (isset($bonuses)) {
                 foreach (array_reverse($bonuses, true) as $i => $bonus) {
                     if ($bonus) {
                         $lastBonusPosition = $i;
                         break;
                     }
                 }
             }
             if (isset($lastBonusPosition)) {
                 foreach (array_slice($bonuses, 0, $lastBonusPosition + 1) as $bonus) {
                     $specifics->append('bonus', $bonus);
                 }
             }
             $fieldsAsMultiDimensionalArray['text'][] = $fieldsAsMultiDimensionalArray['answer'][0];
             if (count($fieldsAsMultiDimensionalArray['answer']) === 1) {
                 unset($fieldsAsMultiDimensionalArray['answer']);
             }
             break;
         case 1:
             // 選択形式
         // 選択形式
         case 3:
             // 全選択形式
             if (empty($fieldsAsMultiDimensionalArray['answer'])) {
                 throw new SyntaxException(_('\\seikai が設定されていません。'));
             }
             $fieldsAsMultiDimensionalArray['text'][] = count($fieldsAsMultiDimensionalArray['answer']) === 1 ? $fieldsAsMultiDimensionalArray['answer'][0] : '「' . implode('」' . ($answerType === 1 ? 'か' : 'と') . '「', $fieldsAsMultiDimensionalArray['answer']) . '」';
             break;
         case 2:
             // 並べ替え形式
             if (isset($numbersAndOptions)) {
                 if (count(end($numbersAndOptions)) === 1) {
                     // 順番が指定されていない選択肢を削除
                     array_pop($numbersAndOptions);
                 }
                 sort($numbersAndOptions);
                 $fieldsAsMultiDimensionalArray['option'] = array_column($numbersAndOptions, 1);
                 $fieldsAsMultiDimensionalArray['text'][] = implode(' → ', $fieldsAsMultiDimensionalArray['option']);
             }
             break;
     }
     $encoded = (string) $specifics;
     if ($encoded !== '') {
         $fieldsAsMultiDimensionalArray['specifics'][] = $encoded;
     }
     try {
         $dictioanry->addWord($fieldsAsMultiDimensionalArray);
         if ($answerType === 0) {
             // 記述形式
             $words = $dictioanry->getWords();
             $word = $words[count($words) - 1];
             if (isset($word['answer'])) {
                 foreach ($word['answer'] as $answer) {
                     $this->wholeText .= $answerValidator->isRegExp($answer) ? preg_replace('#^/|\\.\\*|/$#u', '', $answer) : $answer;
                 }
             } else {
                 $this->wholeText .= $word['text'][0];
             }
         }
     } catch (SyntaxException $e) {
         $this->logInconvertibleError($line, $e);
     }
 }
 /**
  * CSVの一レコードを表す2つの配列によってお題を追加します。
  * @param Dictionary $dictionary
  * @param string[] $fieldNames
  * @param string[] $fields
  * @param bool $first ヘッダ行を除く最初のレコードであれば真。
  * @throws SyntaxException
  */
 protected function addRecord(Dictionary $dictionary, array $fieldNames, array $fields, bool $first)
 {
     if (!in_array('text', $fieldNames)) {
         throw new SyntaxException(sprintf(_('ヘッダ行「%s」にフィールド名「text」が存在しません'), $this->convertToCSVRecord($fieldNames)));
     }
     if (count($fields) > count($fieldNames)) {
         throw new SyntaxException(sprintf(_('「%s」のフィールド数は、ヘッダ行のフィールド名の数を超えています。'), $this->convertToCSVRecord($fields)));
     }
     foreach ($fields as $i => $field) {
         if ($field !== '') {
             if ($fieldNames[$i][0] === '@') {
                 if ($first) {
                     $metaFields[$fieldNames[$i]] = $field;
                 } else {
                     $this->logger->error(sprintf(_('メタフィールド%sの内容は、最初のレコードにのみ記述可能です。'), $fieldNames[$i]));
                 }
             } else {
                 $fieldsAsMultiDimensionalArray[$fieldNames[$i]][] = $field;
             }
         }
     }
     if (!isset($fieldsAsMultiDimensionalArray['text'][0])) {
         throw new SyntaxException(sprintf(_('「%s」にはtextフィールドが存在しません。'), $this->convertToCSVRecord($fields)));
     }
     $dictionary->addWord($fieldsAsMultiDimensionalArray);
     if (isset($metaFields)) {
         $dictionary->setMetadata($metaFields);
     }
 }