8 /** @var array<string,true> */
10 /** @var array<string,array<string,array>> */
14 /** @var callable|false */
15 private $warningCallback;
19 public static function edit( $text, array $deletions, array $changes, $warningCallback = null ) {
20 $editor = new self( $text, $deletions, $changes, $warningCallback );
22 return $editor->result
;
25 private function __construct( $text, array $deletions, array $changes, $warningCallback ) {
26 $this->lines
= explode( "\n", $text );
27 $this->numLines
= count( $this->lines
);
28 $this->deletions
= array_fill_keys( $deletions, true );
29 $this->changes
= $changes;
30 $this->warningCallback
= $warningCallback;
33 private function execute() {
34 while ( $this->pos
< $this->numLines
) {
35 $line = $this->lines
[$this->pos
];
36 switch ( $this->getHeading( $line ) ) {
45 if ( $this->pos
< $this->numLines
- 1 ) {
48 $this->emitComment( $line );
52 foreach ( $this->deletions
as $deletion => $unused ) {
53 $this->warning( "Could not find test \"$deletion\" to delete it" );
55 foreach ( $this->changes
as $test => $sectionChanges ) {
56 foreach ( $sectionChanges as $section => $change ) {
57 $this->warning( "Could not find section \"$section\" in test \"$test\" " .
58 "to {$change['op']} it" );
63 private function warning( $text ) {
64 $cb = $this->warningCallback
;
70 private function getHeading( $line ) {
71 if ( preg_match( '/^!!\s*(\S+)/', $line, $m ) ) {
78 private function parseTest() {
80 $line = $this->lines
[$this->pos++
];
81 $heading = $this->getHeading( $line );
84 'headingLine' => $line,
88 while ( $this->pos
< $this->numLines
) {
89 $line = $this->lines
[$this->pos++
];
90 $nextHeading = $this->getHeading( $line );
91 if ( $nextHeading === 'end' ) {
94 // Add trailing line breaks to the "end" section, to allow for neat deletions
96 while ( $this->lines
[$this->pos
] === '' && $this->pos
< $this->numLines
- 1 ) {
103 'headingLine' => $line,
106 $this->emitTest( $test );
108 } elseif ( $nextHeading !== false ) {
110 $heading = $nextHeading;
113 'headingLine' => $line,
117 $section['contents'] .= "$line\n";
121 throw new OutOfBoundsException( 'Unexpected end of file' );
124 private function parseHooks() {
125 $line = $this->lines
[$this->pos++
];
126 $heading = $this->getHeading( $line );
127 $expectedEnd = 'end' . $heading;
128 $contents = "$line\n";
131 $line = $this->lines
[$this->pos++
];
132 $nextHeading = $this->getHeading( $line );
133 $contents .= "$line\n";
134 } while ( $this->pos
< $this->numLines
&& $nextHeading !== $expectedEnd );
136 if ( $nextHeading !== $expectedEnd ) {
137 throw new UnexpectedValueException( 'Unexpected end of file' );
139 $this->emitHooks( $heading, $contents );
142 protected function emitComment( $contents ) {
143 $this->result
.= $contents;
146 protected function emitTest( $test ) {
148 foreach ( $test as $section ) {
149 if ( $section['name'] === 'test' ) {
150 $testName = rtrim( $section['contents'], "\n" );
153 if ( isset( $this->deletions
[$testName] ) ) {
154 // Acknowledge deletion
155 unset( $this->deletions
[$testName] );
158 if ( isset( $this->changes
[$testName] ) ) {
159 $changes =& $this->changes
[$testName];
160 foreach ( $test as $i => $section ) {
161 $sectionName = $section['name'];
162 if ( isset( $changes[$sectionName] ) ) {
163 $change = $changes[$sectionName];
164 switch ( $change['op'] ) {
166 $test[$i]['name'] = $change['value'];
167 $test[$i]['headingLine'] = "!! {$change['value']}";
170 $test[$i]['contents'] = $change['value'];
173 $test[$i]['deleted'] = true;
176 throw new UnexpectedValueException( "Unknown op: {$change['op']}" );
179 // Note that we use the old section name for the rename op
180 unset( $changes[$sectionName] );
184 foreach ( $test as $section ) {
185 if ( isset( $section['deleted'] ) ) {
188 $this->result
.= $section['headingLine'] . "\n";
189 $this->result
.= $section['contents'];
193 protected function emitHooks( $heading, $contents ) {
194 $this->result
.= $contents;