Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { isWhitespace, isNewline, isParens, isSummarySep } = require('./lib/type-
*/
function message (commitText) {
const scanner = new Scanner(commitText.trim())
const start = scanner.position()
const node = {
type: 'message',
children: []
Expand All @@ -18,7 +19,10 @@ function message (commitText) {
} else {
node.children.push(s)
}
if (scanner.eof()) return node
if (scanner.eof()) {
node.position = { start, end: scanner.position() }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps an an eventual refactor we could have scanner return a Node object, which like you suggest could have an exit method?

return node
}

// <summary> <newline> <body-footer>
if (isNewline(scanner.peek())) {
Expand All @@ -27,6 +31,7 @@ function message (commitText) {
invalidToken(scanner, ['none'])
}
node.children.push(bodyFooter(scanner))
node.position = { start, end: scanner.position() }
return node
}

Expand All @@ -36,6 +41,7 @@ function message (commitText) {
*
*/
function summary (scanner) {
const start = scanner.position()
const node = {
type: 'summary',
children: []
Expand Down Expand Up @@ -73,13 +79,15 @@ function summary (scanner) {
} else {
return invalidToken(scanner, [':', '('])
}
node.position = { start, end: scanner.position() }
return node
}

/*
* <type> ::= 1*<any UTF8-octets except newline or parens or ":" or "!:" or whitespace>
*/
function type (scanner) {
const start = scanner.position()
const node = {
type: 'type',
value: ''
Expand All @@ -94,6 +102,7 @@ function type (scanner) {
if (node.value === '') {
return invalidToken(scanner, ['type'])
} else {
node.position = { start, end: scanner.position() }
return node
}
}
Expand All @@ -102,6 +111,7 @@ function type (scanner) {
* <text> ::= 1*<any UTF8-octets except newline>
*/
function text (scanner) {
const start = scanner.position()
const node = {
type: 'text',
value: ''
Expand All @@ -113,32 +123,45 @@ function text (scanner) {
}
node.value += scanner.next()
}
node.position = { start, end: scanner.position() }
return node
}

/*
* <summary-sep> ::= "!"? ":" *<whitespace>
*/
function summarySep (scanner) {
const start = scanner.position()
const node = {
type: 'summary-sep',
children: []
}
if (isSummarySep(scanner.peek())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do keep the summary-sep node, perhaps I should have called this isBreakingSep?

scanner.next()
// manually offset the end with half the "!:" size
const breakingEnd = scanner.position()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit unfortunate, but I don't see a better way to make this happen.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we could drop the summary-sep node, and just make ! a special case where we check whether the next node is a :, and then add the additional node if needed?

breakingEnd.offset--
breakingEnd.column--
node.children.push({
type: 'breaking-change',
value: '!'
value: '!',
position: { start, end: breakingEnd }
})
// manually offset the start with half the "!:" size
const separatorStart = scanner.position()
separatorStart.offset--
separatorStart.column--
node.children.push({
type: 'separator',
value: ':'
value: ':',
position: { start: separatorStart, end: scanner.position() }
})
} else if (scanner.peek() === ':') {
scanner.next()
node.children.push({
type: 'separator',
value: ':'
value: ':',
position: { start, end: scanner.position() }
})
} else {
return invalidToken(scanner, [':'])
Expand All @@ -151,6 +174,7 @@ function summarySep (scanner) {
* <scope> ::= 1*<any UTF8-octets except newline or parens>
*/
function scope (scanner) {
const start = scanner.position()
const node = {
type: 'scope',
value: ''
Expand All @@ -166,6 +190,7 @@ function scope (scanner) {
if (node.value === '') {
return invalidToken(scanner, ['scope'])
} else {
node.position = { start, end: scanner.position() }
return node
}
}
Expand All @@ -192,13 +217,15 @@ function bodyFooter (scanner) {
node.children.push(f)
}
}
node.position = { start, end: scanner.position() }
return node
}

/*
* <footer> ::= <token> <separator> *<whitespace> <value> <newline>?
*/
function footer (scanner) {
const start = scanner.position()
const node = {
type: 'footer',
children: []
Expand Down Expand Up @@ -230,6 +257,7 @@ function footer (scanner) {
if (isNewline(scanner.peek())) {
scanner.next()
}
node.position = { start, end: scanner.position() }
return node
}

Expand All @@ -251,6 +279,7 @@ function token (scanner) {
scanner.rewind(start)
} else {
node.children.push(b)
node.position = { start, end: scanner.position() }
return node
}

Expand All @@ -273,13 +302,15 @@ function token (scanner) {
scanner.next()
}
}
node.position = { start, end: scanner.position() }
return node
}

/*
* "BREAKING CHANGE"
*/
function breakingChangeLiteral (scanner) {
const start = scanner.position()
const node = {
type: 'breaking-change',
value: ''
Expand All @@ -291,6 +322,7 @@ function breakingChangeLiteral (scanner) {
if (node.value === '') {
return invalidToken(scanner, ['BREAKING CHANGE'])
} else {
node.position = { start, end: scanner.position() }
return node
}
}
Expand All @@ -300,6 +332,7 @@ function breakingChangeLiteral (scanner) {
* | <text>
*/
function value (scanner) {
const start = scanner.position()
const node = {
type: 'value',
children: []
Expand All @@ -310,6 +343,7 @@ function value (scanner) {
while (!((c = continuation(scanner)) instanceof Error)) {
node.children.push(c)
}
node.position = { start, end: scanner.position() }
return node
}

Expand All @@ -334,6 +368,7 @@ function continuation (scanner) {
} else {
return invalidToken(scanner, ['continuation'])
}
node.position = { start, end: scanner.position() }
return node
}

Expand Down Expand Up @@ -366,14 +401,16 @@ function separator (scanner) {
} else {
return invalidToken(scanner, ['separator'])
}
node.position = { start, end: scanner.position() }
return node
}

function invalidToken (scanner, expected) {
if (scanner.eof()) {
return Error(`unexpected token EOF valid tokens [${expected.join(', ')}]`)
} else {
return Error(`unexpected token '${scanner.peek()}' at position ${scanner.position()} valid tokens [${expected.join(', ')}]`)
const pos = scanner.position()
return Error(`unexpected token '${scanner.peek()}' at position ${pos.line}:${pos.column} valid tokens [${expected.join(', ')}]`)
}
}

Expand Down
40 changes: 22 additions & 18 deletions lib/scanner.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
const { isWhitespace } = require('./type-checks')
const { isWhitespace, isNewline } = require('./type-checks')
const { CR, LF } = require('./codes')

class Scanner {
constructor (text) {
this.pos = 0
constructor (text, pos) {
this.text = text
this.pos = pos ? { ...pos } : { line: 1, column: 1, offset: 0 }
}

eof () {
return this.pos >= this.text.length
return this.pos.offset >= this.text.length
}

next (n) {
if (n) {
const token = this.text.substring(this.pos, this.pos + n)
this.pos += n
return token
} else {
const token = this.peek()
this.pos += token.length
return token
const token = n
? this.text.substring(this.pos.offset, this.pos.offset + n)
: this.peek()

this.pos.offset += token.length
this.pos.column += token.length

if (isNewline(token)) {
this.pos.line++
this.pos.column = 1
}

return token
}

nextIgnoreWhitespace () {
Expand All @@ -31,29 +35,29 @@ class Scanner {

consumeWhitespace () {
while (isWhitespace(this.peek())) {
this.pos++
this.next()
}
}

peek () {
let token = this.text.charAt(this.pos)
let token = this.text.charAt(this.pos.offset)
// Consume <CR>? <LF>
if (token === CR && this.text.charAt(this.pos + 1) === LF) {
if (token === CR && this.text.charAt(this.pos.offset + 1) === LF) {
token += LF
// Consume ?: separator
} else if (token === '!' && this.text.charAt(this.pos + 1) === ':') {
} else if (token === '!' && this.text.charAt(this.pos.offset + 1) === ':') {
token += ':'
}
return token
}

peekLiteral (literal) {
const str = this.text.substring(this.pos, this.pos + literal.length)
const str = this.text.substring(this.pos.offset, this.pos.offset + literal.length)
return literal === str
}

position () {
return this.pos
return { ...this.pos }
}

rewind (pos) {
Expand Down
4 changes: 2 additions & 2 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ describe('<message>', () => {
it('throws error when ":" token is missing', () => {
expect(() => {
parser('feat add support for scopes')
}).to.throw("unexpected token ' ' at position 4 valid tokens [:, (]")
}).to.throw("unexpected token ' ' at position 1:5 valid tokens [:, (]")
expect(() => {
parser('feat( foo ) add support for scopes')
}).to.throw("unexpected token ' ' at position 11 valid tokens [:]")
}).to.throw("unexpected token ' ' at position 1:12 valid tokens [:]")
})
it('throws error when closing ")" token is missing', () => {
expect(() => {
Expand Down
Loading