<?php declare(strict_types=1);
namespace GraphQL\Language;
use GraphQL\Utils\Utils;
/**
* @see \GraphQL\Tests\Language\BlockStringTest
*/
class BlockString
{
/**
* Produces the value of a block string from its parsed raw value, similar to
* CoffeeScript's block string, Python's docstring trim or Ruby's strip_heredoc.
*
* This implements the GraphQL spec's BlockStringValue() static algorithm.
*/
public static function dedentBlockStringLines(string $rawString): string
{
$lines = Utils::splitLines($rawString);
// Remove common indentation from all lines but first.
$commonIndent = self::getIndentation($rawString);
$linesLength = \count($lines);
if ($commonIndent > 0) {
for ($i = 1; $i < $linesLength; ++$i) {
$lines[$i] = \mb_substr($lines[$i], $commonIndent);
}
}
// Remove leading and trailing blank lines.
$startLine = 0;
while ($startLine < $linesLength && self::isBlank($lines[$startLine])) {
++$startLine;
}
$endLine = $linesLength;
while ($endLine > $startLine && self::isBlank($lines[$endLine - 1])) {
--$endLine;
}
// Return a string of the lines joined with U+000A.
return \implode("\n", \array_slice($lines, $startLine, $endLine - $startLine));
}
private static function isBlank(string $str): bool
{
$strLength = \mb_strlen($str);
for ($i = 0; $i < $strLength; ++$i) {
if ($str[$i] !== ' ' && $str[$i] !== '\t') {
return false;
}
}
return true;
}
public static function getIndentation(string $value): int
{
$isFirstLine = true;
$isEmptyLine = true;
$indent = 0;
$commonIndent = null;
$valueLength = \mb_strlen($value);
for ($i = 0; $i < $valueLength; ++$i) {
switch (Utils::charCodeAt($value, $i)) {
case 13: // \r
if (Utils::charCodeAt($value, $i + 1) === 10) {
++$i; // skip \r\n as one symbol
}
// falls through
// no break
case 10: // \n
$isFirstLine = false;
$isEmptyLine = true;
$indent = 0;
break;
case 9: // \t
case 32: // <space>
++$indent;
break;
default:
if (
$isEmptyLine
&& ! $isFirstLine
&& ($commonIndent === null || $indent < $commonIndent)
) {
$commonIndent = $indent;
}
$isEmptyLine = false;
}
}
return $commonIndent ?? 0;
}
/**
* Print a block string in the indented block form by adding a leading and
* trailing blank line. However, if a block string starts with whitespace and is
* a single-line, adding a leading blank line would strip that whitespace.
*/
public static function print(string $value): string
{
$escapedValue = str_replace('"""', '\\"""', $value);
// Expand a block string's raw value into independent lines.
$lines = Utils::splitLines($escapedValue);
$isSingleLine = count($lines) === 1;
// If common indentation is found we can fix some of those cases by adding leading new line
$forceLeadingNewLine = count($lines) > 1;
foreach ($lines as $i => $line) {
if ($i === 0) {
continue;
}
if ($line !== '' && \preg_match('/^\s/', $line) !== 1) {
$forceLeadingNewLine = false;
}
}
// Trailing triple quotes just looks confusing but doesn't force trailing new line
$hasTrailingTripleQuotes = \preg_match('/\\\\"""$/', $escapedValue) === 1;
// Trailing quote (single or double) or slash forces trailing new line
$hasTrailingQuote = \preg_match('/"$/', $value) === 1 && ! $hasTrailingTripleQuotes;
$hasTrailingSlash = \preg_match('/\\\\$/', $value) === 1;
$forceTrailingNewline = $hasTrailingQuote || $hasTrailingSlash;
// add leading and trailing new lines only if it improves readability
$printAsMultipleLines = ! $isSingleLine
|| mb_strlen($value) > 70
|| $forceTrailingNewline
|| $forceLeadingNewLine
|| $hasTrailingTripleQuotes;
$result = '';
// Format a multi-line block quote to account for leading space.
$skipLeadingNewLine = $isSingleLine && \preg_match('/^\s/', $value) === 1;
if (($printAsMultipleLines && ! $skipLeadingNewLine) || $forceLeadingNewLine) {
$result .= "\n";
}
$result .= $escapedValue;
if ($printAsMultipleLines) {
$result .= "\n";
}
return '"""' . $result . '"""';
}
}
|