/s', $code, $matches)) { return []; } $defs = []; foreach ($matches[1] as $grammar) { $defs = array_merge($defs, parse_grammar($grammar)); } return $defs; } function parse_grammar($grammar) { $defTexts = explode("\n\n", trim($grammar)); $defs = []; foreach ($defTexts as $defText) { if (!preg_match('/^([a-zA-Z0-9-]+)(::?)(\s+one\s+of)?\n(.*?)$/s', $defText, $matches)) { throw new \Exception('Invalid definition'); } $rules = array_map('Grammar\parse_rule', explode("\n", $matches[4])); $defs[] = new Definition( $matches[1], $matches[2] === '::', $matches[3] !== '', $rules ); } return $defs; } function parse_rule($rule) { $regex = <<<'REGEX' /(?: '[^']*(?:''[^']*)*' | "[^"]*(?:""[^"]*)*" )(*SKIP)(*F)|\s+/x REGEX; $parts = array_map('Grammar\parse_rule_part', preg_split($regex, trim($rule))); return new Rule($parts); } function parse_rule_part($part) { if (substr($part, -1) === '?') { return new Opt(parse_rule_part(substr($part, 0, -1))); } if ($part[0] === "'") { $contents = str_replace("''", "'", substr($part, 1, -1)); return new Plain($contents); } if ($part[0] === '"') { $contents = str_replace('""', '"', substr($part, 1, -1)); return new Plain($contents); } return new Reference($part); } function render_grammar($defs, $names, $currentFile) { $ctx = new RenderContext; $ctx->names = $names; $ctx->currentFile = $currentFile; $result = []; foreach ($defs as $def) { $result[] = $def->render($ctx); } return "
\n" . implode("\n\n", $result) . "\n
"; } class RenderContext { public $names; public $currentFile; } class Definition { public $name, $isLexical, $isOneOf, $rules; public function __construct($name, $isLexical, $isOneOf, $rules) { $this->name = $name; $this->isLexical = $isLexical; $this->isOneOf = $isOneOf; $this->rules = $rules; } public function render($ctx) { $sep = $this->isLexical ? '::' : ':'; $oneOf = $this->isOneOf ? ' one of' : ''; $result = "name\">$this->name$sep$oneOf"; foreach ($this->rules as $rule) { $result .= "\n " . $rule->render($ctx); } return $result; } } class Rule { public $parts; public function __construct($parts) { $this->parts = $parts; } public function render($ctx) { $parts = []; foreach ($this->parts as $part) { $parts[] = $part->render($ctx); } return implode(' ', $parts); } } class Reference { public $name; public function __construct($name) { $this->name = $name; } public function render($ctx) { if (!isset($ctx->names[$this->name])) { throw new \Exception("Reference to unknown name $this->name"); } $fileName = $ctx->names[$this->name]; $anchor = "#grammar-$this->name"; if ($fileName != $ctx->currentFile) { $anchor = $fileName . $anchor; } return "$this->name"; } } class Plain { public $string; public function __construct($string) { $this->string = $string; } public function render($ctx) { return htmlspecialchars($this->string); } } class Opt { public $inner; public function __construct($inner) { $this->inner = $inner; } public function render($ctx) { return $this->inner->render($ctx) . 'opt'; } }