Skip to content

Commit

Permalink
Docs: Auto-generate human-readable version of Gutenberg block grammar (
Browse files Browse the repository at this point in the history
…#6116)

* Grammar: Add proof-of-concept rule display name for Block_Attributes

* Add PEG.js grammar to parse Gutenberg block grammar

* Docs: Grammar: Add autogenerated doc on Gutenberg block grammar

* Docs: Grammar: Add bin/generate-public-grammar.js, reusing pegjs internals

* Docs: Grammar: Render as HTML

* Docs: Grammar: Remove shell-based docgen helper
  • Loading branch information
mcsf committed May 26, 2018
1 parent 1e689af commit a5917bb
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 25 deletions.
106 changes: 106 additions & 0 deletions bin/generate-public-grammar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env node
const parser = require( '../node_modules/pegjs/lib/parser.js' );
const fs = require( 'fs' );
const path = require( 'path' );
const grammarSource = fs.readFileSync( './blocks/api/post.pegjs', 'utf8' );
const grammar = parser.parse( grammarSource );

function escape( text ) {
return text
.replace( /\t/g, '\\t' )
.replace( /\r/g, '\\r' )
.replace( /\n/g, '\\n' )
.replace( /\&/g, '&' )
.replace( /</g, '&lt;' );
}

function isGroup( expression ) {
return [
'choice',
'action',
'labeled',
'sequence',
].indexOf( expression.type ) >= 0;
}

function flattenUnary( expression ) {
const shouldWrap = isGroup( expression );
const inner = flatten( expression );
return shouldWrap ? '(' + inner + ')' : inner;
}

function flatten( expression ) {
switch ( expression.type ) {
// Terminal
case 'any':
return '.';
case 'rule_ref':
return expression.name;
case 'literal':
return '"' + escape( expression.value ) + '"';
case 'class':
return (
'[' + ( expression.inverted ? '^' : '' ) +
expression.parts.map( ( part ) =>
escape( Array.isArray( part ) ? part.join( '-' ) : part )
).join( '' ) +
']' + ( expression.ignoreCase ? 'i' : '' )
);

// Unary
case 'zero_or_more':
return flattenUnary( expression.expression ) + '*';
case 'one_or_more':
return flattenUnary( expression.expression ) + '+';
case 'optional':
return flattenUnary( expression.expression ) + '?';
case 'simple_not':
return '!' + flattenUnary( expression.expression );

// Other groups
case 'sequence':
return expression.elements.map( flatten ).join( ' ' );
case 'choice':
const sep = expression.isRuleTop ? '\n / ' : ' / ';
return expression.alternatives.map( flatten ).join( sep );
case 'group':
return '(' + flatten( expression.expression ) + ')';
case 'text':
// Avoid double parentheses
const inner = flatten( expression.expression );
const shouldWrap = inner.indexOf( '(' ) !== 0;
return shouldWrap ? '$(' + inner + ')' : '$' + inner;
case 'action':
case 'labeled':
case 'named':
return flatten( expression.expression );

// Top-level formatting
case 'grammar':
return `<dl>${ expression.rules.map( flatten ).join( '' ) }</dl>`;
case 'rule':
expression.expression.isRuleTop = true;
const displayName = expression.expression.type === 'named' ?
expression.expression.name : '';
return `<dt>${ displayName }</dt>` +
`<dd><pre><header>${ expression.name }</header> = ` +
`${ flatten( expression.expression ) }</pre></dd>`;

default:
throw new Error( JSON.stringify( expression ) );
}
}

fs.writeFileSync(
path.join( __dirname, '..', 'docs', 'grammar.md' ), `
# The Gutenberg block grammar
<style>
dl { display: flex; flex-wrap: wrap; font-size: 110%; }
dt, dd { flex: 40%; margin-bottom: 1em; }
dt { text-align: right; font-style: italic; font-size: 105%; }
dd header { font-weight: bold; }
pre { margin: 0; }
</style>
${ flatten( grammar ) }
` );
1 change: 1 addition & 0 deletions blocks/api/post.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ Block_Name_Part
= $( [a-z][a-z0-9_-]* )

Block_Attributes
"JSON-encoded attributes embedded in a block's opening comment"
= attrs:$("{" (!("}" __ """/"? "-->") .)* "}")
{
/** <?php return json_decode( $attrs, true ); ?> **/
Expand Down
13 changes: 13 additions & 0 deletions docs/grammar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

# The Gutenberg block grammar

<style>
dl { display: flex; flex-wrap: wrap; font-size: 110%; }
dt, dd { flex: 40%; margin-bottom: 1em; }
dt { text-align: right; font-style: italic; font-size: 105%; }
dd header { font-weight: bold; }
pre { margin: 0; }
</style>
<dl><dt></dt><dd><pre><header>Block_List</header> = $(!Block .)* (Block $(!Block .)*)* $(.*)</pre></dd><dt></dt><dd><pre><header>Block</header> = Block_Void
/ Block_Balanced</pre></dd><dt></dt><dd><pre><header>Block_Void</header> = "&lt;!--" __ "wp:" Block_Name __ (Block_Attributes __)? "/-->"</pre></dd><dt></dt><dd><pre><header>Block_Balanced</header> = Block_Start (Block / $(!Block_End .))* Block_End</pre></dd><dt></dt><dd><pre><header>Block_Start</header> = "&lt;!--" __ "wp:" Block_Name __ (Block_Attributes __)? "-->"</pre></dd><dt></dt><dd><pre><header>Block_End</header> = "&lt;!--" __ "/wp:" Block_Name __ "-->"</pre></dd><dt></dt><dd><pre><header>Block_Name</header> = Namespaced_Block_Name
/ Core_Block_Name</pre></dd><dt></dt><dd><pre><header>Namespaced_Block_Name</header> = $(Block_Name_Part "/" Block_Name_Part)</pre></dd><dt></dt><dd><pre><header>Core_Block_Name</header> = $(Block_Name_Part)</pre></dd><dt></dt><dd><pre><header>Block_Name_Part</header> = $([a-z] [a-z0-9_-]*)</pre></dd><dt>JSON-encoded attributes embedded in a block's opening comment</dt><dd><pre><header>Block_Attributes</header> = $("{" (!("}" __ "" "/"? "-->") .)* "}")</pre></dd><dt></dt><dd><pre><header>__</header> = [ \t\r\n]+</pre></dd></dl>
60 changes: 35 additions & 25 deletions lib/parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ private function peg_buildException($message, $expected, $pos) {
private $peg_c21;
private $peg_c22;
private $peg_c23;
private $peg_c24;

private function peg_f0($pre, $b, $html) { return array( $b, $html ); }
private function peg_f1($pre, $bs, $post) { return peg_join_blocks( $pre, $bs, $post ); }
Expand Down Expand Up @@ -1106,16 +1107,17 @@ private function peg_parseBlock_Name_Part() {

private function peg_parseBlock_Attributes() {

$this->peg_silentFails++;
$s0 = $this->peg_currPos;
$s1 = $this->peg_currPos;
$s2 = $this->peg_currPos;
if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c17) {
$s3 = $this->peg_c17;
if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c18) {
$s3 = $this->peg_c18;
$this->peg_currPos++;
} else {
$s3 = $this->peg_FAILED;
if ($this->peg_silentFails === 0) {
$this->peg_fail($this->peg_c18);
$this->peg_fail($this->peg_c19);
}
}
if ($s3 !== $this->peg_FAILED) {
Expand All @@ -1124,19 +1126,19 @@ private function peg_parseBlock_Attributes() {
$s6 = $this->peg_currPos;
$this->peg_silentFails++;
$s7 = $this->peg_currPos;
if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c19) {
$s8 = $this->peg_c19;
if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c20) {
$s8 = $this->peg_c20;
$this->peg_currPos++;
} else {
$s8 = $this->peg_FAILED;
if ($this->peg_silentFails === 0) {
$this->peg_fail($this->peg_c20);
$this->peg_fail($this->peg_c21);
}
}
if ($s8 !== $this->peg_FAILED) {
$s9 = $this->peg_parse__();
if ($s9 !== $this->peg_FAILED) {
$s10 = $this->peg_c21;
$s10 = $this->peg_c22;
if ($s10 !== $this->peg_FAILED) {
if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c11) {
$s11 = $this->peg_c11;
Expand Down Expand Up @@ -1217,19 +1219,19 @@ private function peg_parseBlock_Attributes() {
$s6 = $this->peg_currPos;
$this->peg_silentFails++;
$s7 = $this->peg_currPos;
if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c19) {
$s8 = $this->peg_c19;
if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c20) {
$s8 = $this->peg_c20;
$this->peg_currPos++;
} else {
$s8 = $this->peg_FAILED;
if ($this->peg_silentFails === 0) {
$this->peg_fail($this->peg_c20);
$this->peg_fail($this->peg_c21);
}
}
if ($s8 !== $this->peg_FAILED) {
$s9 = $this->peg_parse__();
if ($s9 !== $this->peg_FAILED) {
$s10 = $this->peg_c21;
$s10 = $this->peg_c22;
if ($s10 !== $this->peg_FAILED) {
if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c11) {
$s11 = $this->peg_c11;
Expand Down Expand Up @@ -1306,13 +1308,13 @@ private function peg_parseBlock_Attributes() {
}
}
if ($s4 !== $this->peg_FAILED) {
if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c19) {
$s5 = $this->peg_c19;
if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c20) {
$s5 = $this->peg_c20;
$this->peg_currPos++;
} else {
$s5 = $this->peg_FAILED;
if ($this->peg_silentFails === 0) {
$this->peg_fail($this->peg_c20);
$this->peg_fail($this->peg_c21);
}
}
if ($s5 !== $this->peg_FAILED) {
Expand Down Expand Up @@ -1340,32 +1342,39 @@ private function peg_parseBlock_Attributes() {
$s1 = $this->peg_f8($s1);
}
$s0 = $s1;
$this->peg_silentFails--;
if ($s0 === $this->peg_FAILED) {
$s1 = $this->peg_FAILED;
if ($this->peg_silentFails === 0) {
$this->peg_fail($this->peg_c17);
}
}

return $s0;
}

private function peg_parse__() {

$s0 = array();
if (Gutenberg_PEG_peg_char_class_test($this->peg_c22, $this->input_substr($this->peg_currPos, 1))) {
if (Gutenberg_PEG_peg_char_class_test($this->peg_c23, $this->input_substr($this->peg_currPos, 1))) {
$s1 = $this->input_substr($this->peg_currPos, 1);
$this->peg_currPos++;
} else {
$s1 = $this->peg_FAILED;
if ($this->peg_silentFails === 0) {
$this->peg_fail($this->peg_c23);
$this->peg_fail($this->peg_c24);
}
}
if ($s1 !== $this->peg_FAILED) {
while ($s1 !== $this->peg_FAILED) {
$s0[] = $s1;
if (Gutenberg_PEG_peg_char_class_test($this->peg_c22, $this->input_substr($this->peg_currPos, 1))) {
if (Gutenberg_PEG_peg_char_class_test($this->peg_c23, $this->input_substr($this->peg_currPos, 1))) {
$s1 = $this->input_substr($this->peg_currPos, 1);
$this->peg_currPos++;
} else {
$s1 = $this->peg_FAILED;
if ($this->peg_silentFails === 0) {
$this->peg_fail($this->peg_c23);
$this->peg_fail($this->peg_c24);
}
}
}
Expand Down Expand Up @@ -1407,13 +1416,14 @@ public function parse($input) {
$this->peg_c14 = array( "type" => "class", "value" => "[a-z]", "description" => "[a-z]" );
$this->peg_c15 = array(array(97,122), array(48,57), array(95,95), array(45,45));
$this->peg_c16 = array( "type" => "class", "value" => "[a-z0-9_-]", "description" => "[a-z0-9_-]" );
$this->peg_c17 = "{";
$this->peg_c18 = array( "type" => "literal", "value" => "{", "description" => "\"{\"" );
$this->peg_c19 = "}";
$this->peg_c20 = array( "type" => "literal", "value" => "}", "description" => "\"}\"" );
$this->peg_c21 = "";
$this->peg_c22 = array(array(32,32), array(9,9), array(13,13), array(10,10));
$this->peg_c23 = array( "type" => "class", "value" => "[ \t\r\n]", "description" => "[ \t\r\n]" );
$this->peg_c17 = array("type" => "other", "description" => "JSON-encoded attributes embedded in a block's opening comment" );
$this->peg_c18 = "{";
$this->peg_c19 = array( "type" => "literal", "value" => "{", "description" => "\"{\"" );
$this->peg_c20 = "}";
$this->peg_c21 = array( "type" => "literal", "value" => "}", "description" => "\"}\"" );
$this->peg_c22 = "";
$this->peg_c23 = array(array(32,32), array(9,9), array(13,13), array(10,10));
$this->peg_c24 = array( "type" => "class", "value" => "[ \t\r\n]", "description" => "[ \t\r\n]" );

$peg_startRuleFunctions = array( 'Block_List' => array($this, "peg_parseBlock_List") );
$peg_startRuleFunction = array($this, "peg_parseBlock_List");
Expand Down

0 comments on commit a5917bb

Please sign in to comment.