code = $code; } public static function getBeginManualSection(string $id): string { return sprintf('/* BEGIN MANUAL SECTION %s */', $id); } private static function getBeginManualSectionRegex(string $regex): string { // this needs to be kept in sync with getBeginManualSection. return sprintf('|/\* BEGIN MANUAL SECTION %s \*/|', $regex); } public static function getEndManualSection(): string { return '/* END MANUAL SECTION */'; } public static function containsManualSection(string $code): bool { return strpos($code, self::getEndManualSection()) !== false; } /** * Merge the code with the existing code. The manual sections of * the existing code will be merged into the corresponding sections * of the new code. * * If rekeys is specified, we will attempt to pull code from sections * with different names, as specified by the mapping. */ public function merge( string $existing_code, ?Map> $rekeys = null): string { $merged = array(); $existing = $this->extractManualCode($existing_code); $generated = $this->iterateCodeSections($this->code); foreach ($generated as $section) { list($id, $chunk) = $section; if ($id === null) { // Autogenerated section, add it as it is $merged[] = $chunk; } else { if ($existing->containsKey($id)) { // This manual section was present in the existing code, so insert it $merged[] = $existing[$id]; } else { $content = array(); if ($rekeys !== null) { if ($rekeys->containsKey($id)) { foreach ($rekeys[$id] as $old_id) { if ($existing->containsKey($old_id)) { $content[] = $existing[$old_id]; } } } } if ($content) { $merged[] = implode("\n\n", $content); } else { // This manual section is new, so insert inside it the chunk from // the generated code (e.g. the generated code can have a comment // saying what that manual section should be used for) $merged[] = $chunk; } } } } return implode("\n", array_filter($merged)); } /** * Extract manually generated code and returns a map of ids to chunks of code */ private function extractManualCode(string $code): Map { $manual = Map {}; foreach ($this->iterateCodeSections($code) as $section) { list($id, $chunk) = $section; if ($id !== null) { $manual[$id] = $chunk; } } return $manual; } /** * Extract the generated code and returns it as a string. */ public function extractGeneratedCode(): string { $generated = array(); foreach ($this->iterateCodeSections($this->code) as $section) { list($id, $chunk) = $section; if ($id === null) { $generated[] = $chunk; } } return implode("\n", $generated); } /** * Validate the manual sections and throws PartiallyGeneratedCodeException * if there are any errors (e.g. unfinished manual section, nested * manual sections, duplicated ids, etc) */ public function assertValidManualSections(): void { foreach ($this->iterateCodeSections($this->code) as $section) {} } /** * Iterates through the code yielding tuples of ($id, $chunk), where * $id is the id of the manual section or null if it's an auto-generated * section, and chunk is the code belonging to that section. * The lines containing begin/end of manual section belong to the * autogenerated sections. */ private function iterateCodeSections( string $code ): \Generator { // Regular expression to match the beginning of a manual section $quoted = preg_quote(self::$manualBegin, '/'); $begin = self::getBeginManualSectionRegex('(.*)'); $valid_begin = self::getBeginManualSectionRegex('([A-Za-z0-9:_]+)'); $seen_ids = Set {}; $current_id = null; $chunk = array(); $manual = array(); $lines = explode("\n", $code); foreach ($lines as $line) { if (strpos($line, self::$manualEnd) !== false) { yield tuple($current_id, implode("\n", $chunk)); $chunk = array($line); $current_id = null; } else if (preg_match($begin, $line) === 1) { if ($current_id !== null) { throw new PartiallyGeneratedCodeException( "The manual section $current_id was open before ". "the previous one was closed" ); } if (!preg_match($valid_begin, $line)) { throw new PartiallyGeneratedCodeException( "Invalid id specified: " . $line ); } $chunk[] = $line; yield tuple(null, implode("\n", $chunk)); $chunk = array(); $current_id = trim(preg_replace($begin, '\\1', $line)); if ($seen_ids->contains($current_id)) { throw new PartiallyGeneratedCodeException( "Duplicate manual section id: $current_id" ); } $seen_ids->add($current_id); } else { $chunk[] = $line; } } if ($current_id !== null) { throw new PartiallyGeneratedCodeException( "The manual section $current_id was not closed at the end of code" ); } if ($code) { yield tuple(null, implode("\n", $chunk)); } } } final class PartiallyGeneratedCodeException extends \Exception {};