-
Notifications
You must be signed in to change notification settings - Fork 10
/
scanner.js
141 lines (127 loc) · 3.9 KB
/
scanner.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// @ts-check
'use strict';
// Transforms a stream of text into a sequence of 'lines', tracking each line's
// level of indentation.
// Trims lines.
// Stips comments.
//
// The scanner feeds into an outline lexer.
const tabWidth = 4;
/**
* @param {number} columnNo -- screen column number (logical X coord)
* @returns {number} -- column number where the next tab stop starts
*/
function nextTabStop(columnNo) {
// TODO simplify with modulo arithmetic
return Math.floor((columnNo + tabWidth) / tabWidth) * tabWidth;
}
var leaders = '-+*!>';
module.exports = class Scanner {
debug = typeof process === 'object' && process.env.DEBUG_SCANNER
indent = 0
lineStart = 0
indentStart = 0
itemStart = 0
lineNo = 0
columnNo = 0
columnStart = 0
leading = true
leader = ''
/** An Iterator-like object that has text pushed into it by a Scanner.
*
* Its biggest difference from an Iterator<string> is that the Scanner
* object itself is passed along as an additional next agument
*
* @typedef {object} ScanIt
* @prop {(text: string, sc: Scanner) => void} next
* @prop {(sc: Scanner) => void} return
*/
/**
* @param {ScanIt} generator
* @param {string} fileName
*/
constructor(generator, fileName) {
this.generator = generator;
this.fileName = fileName || '-';
}
/**
* @param {string} text
* @returns {void}
*/
next(text) {
for (var i = 0; i < text.length; i++) {
var c = text[i];
var d = text[i + 1];
if (this.debug) {
console.error('SCN', this.position() + ':' + i, JSON.stringify(c + (d || '')));
}
if (
((c === '\t' || c === ' ') && d === '#') ||
(this.columnNo === 0 && c === '#')
) {
this.newLine(text, i);
for (i++; i < text.length; i++) {
c = text[i];
if (c === '\n') {
break;
}
}
} else if (c === '\t') {
this.columnNo = nextTabStop(this.columnNo);
} else if (c === '\n') {
this.newLine(text, i);
} else if (c === ' ') {
this.columnNo++;
} else if (
this.leading && leaders.indexOf(c) >= 0 &&
(d === ' ' || d === '\t')
) {
this.leader += c;
this.columnNo++;
} else if (this.leading && leaders.indexOf(c) >= 0 && d === '\n') {
this.leader += c;
this.indentStart = i;
this.columnStart = this.columnNo;
this.lineStart = this.lineNo;
this.indent = this.columnNo + 2;
} else if (this.leading) {
this.indent = this.columnNo;
this.indentStart = i;
this.columnStart = this.columnNo;
this.lineStart = this.lineNo;
this.columnNo++;
this.leading = false;
}
}
// TODO To exercise the following block, you need a file with no final
// newline.
if (!this.leading) {
this.generator.next(text.slice(this.indentStart, i), this);
}
}
/**
* @param {string} text
* @param {number} i
* @returns {void}
*/
newLine(text, i) {
if (this.leading) {
this.indentStart = i;
}
this.leading = true;
this.generator.next(text.slice(this.indentStart, i), this);
this.columnNo = 0;
this.lineNo++;
this.lineStart = i + 1;
this.leader = '';
}
/**
* @returns {void}
*/
return() {
this.generator.return(this);
}
position() {
return this.fileName + ':' + (this.lineNo + 1) + ':' + (this.columnStart + 1);
}
}