Skip to content

Commit

Permalink
feat: support BREAKING CHANGE in body (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcoe authored Dec 29, 2020
1 parent 2a2474b commit 8f3f5c7
Show file tree
Hide file tree
Showing 4 changed files with 1,397 additions and 109 deletions.
90 changes: 47 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,49 +42,53 @@ in sync with the written specification on conventionalcommits.org.

```ebnf
/* See: https://tools.ietf.org/html/rfc3629#section-4 */
<UTF8-char> ::= "Placeholder for UTF-8 grammar"
<UTF8-octets> ::= <UTF8char>+
<CR> ::= "0x000D"
<LF> ::= "0x000A"
<newline> ::= [<CR>], <LF>
<parens> ::= "(" | ")"
<ZWNBSP> ::= "U+FEFF"
<TAB> ::= "U+0009"
<VT> ::= "U+000B"
<FF> ::= "U+000C"
<SP> ::= "U+0020"
<NBSP> ::= "U+00A0"
<UTF8-char> ::= "Placeholder for UTF-8 grammar"
<UTF8-octets> ::= <UTF8char>+
<CR> ::= "0x000D"
<LF> ::= "0x000A"
<newline> ::= [<CR>], <LF>
<parens> ::= "(" | ")"
<ZWNBSP> ::= "U+FEFF"
<TAB> ::= "U+0009"
<VT> ::= "U+000B"
<FF> ::= "U+000C"
<SP> ::= "U+0020"
<NBSP> ::= "U+00A0"
/* See: https://www.ecma-international.org/ecma-262/11.0/index.html#sec-white-space */
<USP> ::= "Any other Unicode 'Space_Separator' code point"
<USP> ::= "Any other Unicode 'Space_Separator' code point"
/* Any non-newline whitespace: */
<whitespace> ::= <ZWNBSP> | <TAB> | <VT> | <FF> | <SP> | <NBSP> | <USP>
<message> ::= <summary>, <newline>+, <body>, <newline>*, <footer>+
| <summary>, <newline>*, <footer>+
| <summary>, <newline>*
<summary> ::= <type>, "(", <scope>, ")", ["!"], ":", <whitespace>*, <text>
| <type>, ["!"], ":", <whitespace>*, <text>
<type> ::= <any UTF8-octets except newline or parens or ":" or "!:" or whitespace>+
<scope> ::= <any UTF8-octets except newline or parens>+
<text> ::= <any UTF8-octets except newline>*
/*
* Note: if the first <body> node starts with "BREAKING CHANGE:" this should
* be treated by parsers as a breaking change marker upstream:
*/
<body> ::= [<any text except pre-footer>], <newline>, <body>*
| [<any text except pre-footer>]
/* Note: <pre-footer> is used during parsing, but never returned in the AST. */
<pre-footer> ::= <newline>*, <footer>+
<footer> ::= <token>, <separator>, <whitespace>*, <value>, [<newline>]
<token> ::= "BREAKING CHANGE"
| <type>, "(" <scope> ")", ["!"]
| <type>, ["!"]
<separator> ::= ":" | " #"
<value> ::= <text>, <continuation>+
| <text>
<continuation> ::= <newline>, <whitespace>+, <text>
<whitespace> ::= <ZWNBSP> | <TAB> | <VT> | <FF> | <SP> | <NBSP> | <USP>
<message> ::= <summary>, <newline>+, <body>, <newline>*, <footer>+
| <summary>, <newline>*, <footer>+
| <summary>, <newline>*
/* "!" should be added to the AST as a <breaking-change> node with the value "!" */
<summary> ::= <type>, "(", <scope>, ")", ["!"], ":", <whitespace>*, <text>
| <type>, ["!"], ":", <whitespace>*, <text>
<type> ::= <any UTF8-octets except newline or parens or ":" or "!:" or whitespace>+
<scope> ::= <any UTF8-octets except newline or parens>+
<text> ::= <any UTF8-octets except newline>*
<body> ::= [<any body-text except pre-footer>], <newline>, <body>*
| [<any body-text except pre-footer>]
/* For convenience the <breaking-change>, <separator>, <whitespace>, and
* <text> tokens of <body-text> should be appended as children to <body> */
<body-text> ::= [<breaking-change>, ":", <whitespace>*], text
/* Note: <pre-footer> is used during parsing, but not returned in the AST. */
<pre-footer> ::= <newline>*, <footer>+
<footer> ::= <token>, <separator>, <whitespace>*, <value>, [<newline>]
/* "!" should be added to the AST as a <breaking-change> node with the value "!" */
<token> ::= <breaking-change>
| <type>, "(" <scope> ")", ["!"]
| <type>, ["!"]
<separator> ::= ":" | " #"
<value> ::= <text>, <continuation>+
| <text>
<continuation> ::= <newline>, <whitespace>+, <text>
<breaking-change> ::= "BREAKING CHANGE" | "BREAKING-CHANGE"
```
26 changes: 19 additions & 7 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,25 @@ function scope (scanner) {
}

/*
* <body> ::= [<any text except pre-footer>], <newline>, <body>*
* | [<any text except pre-footer>]
* <body> ::= [<any body-text except pre-footer>], <newline>, <body>*
* | [<any body-text except pre-footer>]
*/
function body (scanner) {
const node = scanner.enter('body', [])
// <any text except pre-footer>

// check except <pre-footer> condition:
const pf = preFooter(scanner)
if (!(pf instanceof Error)) return scanner.abort(node)

// ["BREAKING CHANGE", ":", <whitespace>*]
const b = breakingChange(scanner, false)
if (!(b instanceof Error) && scanner.peek() === ':') {
node.children.push(b)
node.children.push(separator(scanner))
const w = whitespace(scanner)
if (!(w instanceof Error)) node.children.push(w)
}

// [<text>]
const t = text(scanner)
node.children.push(t)
Expand Down Expand Up @@ -282,13 +292,15 @@ function token (scanner) {
}

/*
* <breaking-change> ::= "!" | "BREAKING CHANGE"
* <breaking-change> ::= "!" | "BREAKING CHANGE" | "BREAKING-CHANGE"
*
* Note: "!" is only allowed in <footer> and <summary>, not <body>.
*/
function breakingChange (scanner) {
function breakingChange (scanner, allowBang = true) {
const node = scanner.enter('breaking-change', '')
if (scanner.peek() === '!') {
if (scanner.peek() === '!' && allowBang) {
node.value = scanner.next()
} else if (scanner.peekLiteral('BREAKING CHANGE')) {
} else if (scanner.peekLiteral('BREAKING CHANGE') || scanner.peekLiteral('BREAKING-CHANGE')) {
node.value = scanner.next('BREAKING CHANGE'.length)
}
if (node.value === '') {
Expand Down
18 changes: 17 additions & 1 deletion test/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ describe('<message>', () => {
parsed.should.matchSnapshot()
})
it('parses BREAKING CHANGE literal as <token>', () => {
const parsed = parser('fix: address major bug\nBREAKING CHANGE: this change is breaking')
let parsed = parser('fix: address major bug\nBREAKING CHANGE: this change is breaking')
parsed.should.matchSnapshot()
parsed = parser('fix: address major bug\nBREAKING-CHANGE: this change is breaking')
parsed.should.matchSnapshot()
})
it('supports multiline BREAKING CHANGES, via continuation', () => {
Expand Down Expand Up @@ -102,11 +104,25 @@ describe('<message>', () => {
assertNodePositions('fix: address major bug\n\nthis is a free form body of text')
})
})
describe('<body>', () => {
it('parses BREAKING CHANGE at start of body', () => {
const parsed = parser('feat: breaking change\n\nBREAKING CHANGE: introduces breaking change\nsecond line')
parsed.should.matchSnapshot()
})
})
describe('<body>, <newline>*, <footer>+', () => {
it('parses footer after body', () => {
const parsed = parser('fix: address major bug\n\nthis is a free form body of text\nAuthor: @bcoe\nRefs #392')
parsed.should.matchSnapshot()
})
it('parses footer after body containing BREAKING CHANGE', () => {
const parsed = parser('fix: address major bug\n\nBREAKING CHANGE: this is breaking.\nthis is a free form body of text\nAuthor: @bcoe\nRefs #392')
parsed.should.matchSnapshot()
})
it('parses BREAKING CHANGE footers with higher precedence than body', () => {
const parsed = parser('fix: address major bug\n\nBREAKING CHANGE: this is breaking.\n\nAuthor: @bcoe\nRefs #392')
parsed.should.matchSnapshot()
})
it('parses footer after multi-line body', () => {
const parsed = parser('fix: address major bug\n\nthis is the first line of the body\n\nthis is the second line of body\n\nAuthor: @bcoe\nRefs #392')
parsed.should.matchSnapshot()
Expand Down
Loading

0 comments on commit 8f3f5c7

Please sign in to comment.