PageRenderTime 54ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/rainlab/builder/classes/LanguageMixer.php

https://gitlab.com/gideonmarked/yovelife
PHP | 201 lines | 145 code | 38 blank | 18 comment | 20 complexity | df79709ac60c0a38a27e1ce78e86e31a MD5 | raw file
  1. <?php namespace RainLab\Builder\Classes;
  2. use Yaml;
  3. use Symfony\Component\Yaml\Dumper as YamlDumper;
  4. use ApplicationException;
  5. class LanguageMixer
  6. {
  7. /**
  8. * Merges two localization languages and return merged YAML string and indexes of added lines.
  9. */
  10. public function addStringsFromAnotherLanguage($destContents, $srcArray)
  11. {
  12. // 1. Find array keys that exists in the source array and don't exist in the destination array
  13. // 2. Merge the arrays recursively
  14. // 3. To find which lines were added:
  15. // 3.1 get a YAML representation of the destination array
  16. // 3.2 walk through the missing paths obtained in step 1 and for each path:
  17. // 3.3 find its path and its corresponding line in the string from 3.1.
  18. $result = [
  19. 'strings' => '',
  20. 'mismatch' => false,
  21. 'updatedLines' => [],
  22. ];
  23. try
  24. {
  25. $destArray = Yaml::parse($destContents);
  26. }
  27. catch (Exception $ex) {
  28. throw new ApplicationException(sprintf('Cannot parse the YAML content: %s', $ex->getMessage()));
  29. }
  30. if (!$destArray) {
  31. $result['strings'] = $this->arrayToYaml($srcArray);
  32. return $result;
  33. }
  34. $mismatch = false;
  35. $missingPaths = $this->findMissingPaths($destArray, $srcArray, $mismatch);
  36. $mergedArray = self::arrayMergeRecursive($srcArray, $destArray);
  37. $destStrings = $this->arrayToYaml($mergedArray);
  38. $addedLines = $this->getAddedLines($destStrings, $missingPaths);
  39. $result['strings'] = $destStrings;
  40. $result['updatedLines'] = $addedLines['lines'];
  41. $result['mismatch'] = $mismatch || $addedLines['mismatch'];
  42. return $result;
  43. }
  44. public static function arrayMergeRecursive(&$array1, &$array2)
  45. {
  46. // The native PHP implementation of array_merge_recursive
  47. // generates unexpected results when two scalar elements with a
  48. // same key is found, so we use a custom one.
  49. $result = $array1;
  50. foreach ($array2 as $key=>&$value)
  51. {
  52. if (is_array ($value) && isset($result[$key]) && is_array($result[$key]))
  53. {
  54. $result[$key] = self::arrayMergeRecursive($result[$key], $value);
  55. }
  56. else
  57. {
  58. $result[$key] = $value;
  59. }
  60. }
  61. return $result;
  62. }
  63. protected function findMissingPaths($destArray, $srcArray, &$mismatch)
  64. {
  65. $result = [];
  66. $mismatch = false;
  67. $this->findMissingPathsRecursive($destArray, $srcArray, $result, [], $mismatch);
  68. return $result;
  69. }
  70. protected function findMissingPathsRecursive($destArray, $srcArray, &$result, $currentPath, &$mismatch)
  71. {
  72. foreach ($srcArray as $key=>$value) {
  73. $newPath = array_merge($currentPath, [$key]);
  74. $pathValue = null;
  75. $pathExists = $this->pathExistsInArray($destArray, $newPath, $pathValue);
  76. if (!$pathExists) {
  77. $result[] = $newPath;
  78. }
  79. if (is_array($value)) {
  80. $this->findMissingPathsRecursive($destArray, $value, $result, $newPath, $mismatch);
  81. }
  82. else {
  83. // Detect the case when the value in the destination file
  84. // is an array, when the value in the source file a is a string.
  85. if ($pathExists && is_array($pathValue)) {
  86. $mismatch = true;
  87. }
  88. }
  89. }
  90. }
  91. protected function pathExistsInArray($array, $path, &$value)
  92. {
  93. $currentArray = $array;
  94. while ($path) {
  95. $currentPath = array_shift($path);
  96. if (!is_array($currentArray)) {
  97. return false;
  98. }
  99. if (!array_key_exists($currentPath, $currentArray)) {
  100. return false;
  101. }
  102. $currentArray = $currentArray[$currentPath];
  103. }
  104. $value = $currentArray;
  105. return true;
  106. }
  107. protected function arrayToYaml($array)
  108. {
  109. $dumper = new YamlDumper();
  110. return $dumper->dump($array, 20, 0, false, true);
  111. }
  112. protected function getAddedLines($strings, $paths)
  113. {
  114. $result = [
  115. 'lines' => [],
  116. 'mismatch' => false
  117. ];
  118. foreach ($paths as $path) {
  119. $line = $this->getLineForPath($strings, $path);
  120. if ($line !== false) {
  121. $result['lines'][] = $line;
  122. }
  123. else {
  124. $result['mismatch'] = true;
  125. }
  126. }
  127. return $result;
  128. }
  129. protected function getLineForPath($strings, $path)
  130. {
  131. $strings = str_replace("\n\r", "\n", trim($strings));
  132. $lines = explode("\n", $strings);
  133. $lineCount = count($lines);
  134. $currentLineIndex = 0;
  135. foreach ($path as $indentaion=>$key) {
  136. $expectedKeyDefinition = str_repeat(' ', $indentaion).$key.':';
  137. $firstLineAfterKey = true;
  138. for ($lineIndex = $currentLineIndex; $lineIndex < $lineCount; $lineIndex++) {
  139. $line = $lines[$lineIndex];
  140. if (!$firstLineAfterKey) {
  141. $lineIndentation = 0;
  142. if (preg_match('/^\s+/', $line, $matches)) {
  143. $lineIndentation = strlen($matches[0])/4;
  144. }
  145. if ($lineIndentation < $indentaion) {
  146. continue; // Don't allow entering wrong branches
  147. }
  148. }
  149. $firstLineAfterKey = false;
  150. if (strpos($line, $expectedKeyDefinition) === 0) {
  151. $currentLineIndex = $lineIndex;
  152. continue 2;
  153. }
  154. }
  155. // If the key wasn't found in the text, there is
  156. // a structure difference between the source an destination
  157. // languages - for example when a string key was replaced
  158. // with an array of strings.
  159. return false;
  160. }
  161. return $currentLineIndex;
  162. }
  163. }